4.10. Интерфейсы связи - SPI.md


Мы уже познакомились с UART для простой связи точка-точка и с I2C для подключения множества устройств на одну шину. Но что, если нам нужна максимальная скорость? Например, для потоковой передачи данных на цветной дисплей или для быстрой записи на SD-карту. В таких задачах на сцену выходит SPI (Serial Peripheral Interface) — последовательный периферийный интерфейс.

SPI — это, пожалуй, самый быстрый из трёх последовательных интерфейсов, которые мы рассматриваем. Он, как и I2C, является синхронным, но его архитектура позволяет достигать гораздо более высоких скоростей передачи данных.

Принцип работы SPI - прямое соединение

Если I2C можно сравнить с автобусом, где все ждут на одной дороге, то SPI больше похож на прямую телефонную линию между двумя абонентами. Связь всегда происходит между одним Ведущим (Master, наш Рудирон) и одним Ведомым (Slave).

Для базового соединения SPI использует четыре провода:

  • SCLK (Serial Clock) - линия тактирования. Сигналы по ней генерирует Ведущий, задавая темп обмена данными.
  • MOSI (Master Out, Slave In) - линия, по которой Ведущий передаёт данные Ведомому.
  • MISO (Master In, Slave Out) - линия, по которой Ведомый передаёт данные Ведущему.
  • SS (Slave Select) / CS (Chip Select) линия выбора ведомого. Это очень важный пин. Ведущий использует его, чтобы «активировать» конкретное ведомое устройство, с которым он хочет сейчас общаться. На пине SS активного устройства должен быть установлен низкий уровень (LOW).

Преимущество такой архитектуры в том, что данные могут передаваться одновременно в обе стороны (полнодуплексный режим) - Рудирон может отправлять байт на SD-карту и в то же самое время получать другой байт от неё.

Подключение нескольких устройств

“Но как же подключить несколько устройств, если линия прямая?” — спросите вы. Очень просто! Линии SCLK, MOSI и MISO остаются общими для всех устройств, а вот линия SS для каждого ведомого будет своя, идущая от отдельного GPIO пина Рудирона.

Когда Рудирону нужно поговорить, например, с дисплеем, он устанавливает LOW на пине SS, подключённом к дисплею, и HIGH на всех остальных пинах SS. Таким образом, только дисплей будет «слушать» команды, а остальные устройства на шине будут их игнорировать.

SPI на плате Рудирон

Рудирон предоставляет целых два аппаратных SPI-интерфейса, что является большим преимуществом для сложных проектов.

Основной SPI (SPI1):

  • MOSI: Пин 3 (F0).
  • MISO: Пин 0 (F3).
  • SCLK: Пин 2 (F1).
  • SS: По умолчанию для библиотеки SPI может использоваться пин 1 (F2), но вы можете выбрать любой другой GPIO пин для управления линией SS.

Дополнительный SPI (SPI2):

  • MOSI: Пин 21 (D6).
  • MISO: Пин 24 (D2).
  • SCLK: Пин 22 (D5).
  • SS: Любой свободный GPIO пин.

Библиотека SPI

Для работы с интерфейсом используется стандартная библиотека SPI.

Инициализация — SPI.begin() В setup() необходимо инициализировать интерфейс.

#include <SPI.h> // Подключаем библиотеку

void setup() {
  SPI.begin(); // Инициализируем SPI
}

Настройка транзакции — SPI.beginTransaction() Перед тем, как начать обмен данными с конкретным устройством, хорошей практикой является настройка параметров SPI (скорость, порядок бит и режим работы) в соответствии с требованиями этого устройства.

// Настраиваем SPI для нашего устройства: 4 МГц, старший бит вперёд, режим 0
SPISettings mySettings(4000000, MSBFIRST, SPI_MODE0);

void loop() {
  SPI.beginTransaction(mySettings);
  // Здесь будет код обмена данными
  ...
  SPI.endTransaction();
}

Обмен данными — SPI.transfer() Это ключевая функция для отправки и приёма данных. Она одновременно отправляет байт и принимает байт.

byte incomingByte = SPI.transfer(outgoingByte);
  • outgoingByte — байт, который вы хотите отправить.
  • incomingByte — байт, который был получен от ведомого в тот же самый момент. Если вам не нужно ничего получать, вы можете просто проигнорировать возвращаемое значение.

Пример (условное чтение из SPI-устройства):

#include <SPI.h>

const int ssPin = 1; // Используем пин 1 как Slave Select

void setup() {
  Serial.begin(9600);
  SPI.begin();
  pinMode(ssPin, OUTPUT);
  digitalWrite(ssPin, HIGH); // Устройство неактивно
}

void loop() {
  digitalWrite(ssPin, LOW); // Активируем ведомое устройство

  // Отправляем команду "прочитать регистр 0x10"
  SPI.transfer(0x10); 
  // Отправляем "пустой" байт, чтобы получить ответ
  byte data = SPI.transfer(0x00); 

  digitalWrite(ssPin, HIGH); // Деактивируем ведомое устройство

  Serial.print("Получены данные: 0x");
  Serial.println(data, HEX);
  
  delay(1000);
}

Зачем это нужно?

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

  • Работа с памятью: чтение и запись на SD-карты и микросхемы Flash-памяти.
  • Дисплеи: быстрый вывод графики на TFT и OLED экраны.
  • Сетевые модули: подключение Ethernet-контроллеров (W5500) и радиомодулей (NRF24L01).
  • Быстрые преобразователи: работа с высокоскоростными АЦП и ЦАП.

Поздравляем! Вы завершили изучение основных инструментов для программирования микроконтроллеров. Вы научились управлять цифровыми и аналоговыми сигналами, использовать прерывания для мгновенной реакции, организовывать многозадачность и общаться с внешним миром через интерфейсы UART, I2C и SPI. Теперь у вас есть все необходимые знания, чтобы приступить к созданию собственных, даже самых смелых проектов. Удачи в ваших начинаниях