Цифрові інтерфейси зв'язку: SPI

Швидкий і високопродуктивний спосіб обміну даними між мікроконтролерами та периферійними пристроями через повнодуплексну синхронну шину з окремою лінією вибору пристрою

Інтерфейс I2C добре підходить для підключення великої кількості пристроїв лише двома лініями. Але невисока швидкість передачі, напівдуплексний режим роботи, залежність від підтягуючих резисторів і чутливість до перешкод роблять його не найкращим вибором для задач, де важлива продуктивність і стабільність. Особливо це помітно при роботі з дисплеями, пам’яттю або високочастотними сенсорами.

Саме тут на сцену виходить SPI — інтерфейс, який жертвує простотою підключення заради швидкості та надійності. Він дозволяє передавати дані значно швидше, підтримує повнодуплексний режим і забезпечує більш передбачувану роботу без складних механізмів арбітражу шини. SPI стає логічним вибором у системах, де важлива кожна мілісекунда і немає місця для компромісів.

SPI (від англ. Serial Peripheral Interface) - це високошвидкісний, синхронний, повнодуплексний, послідовний протокол зв'язку. Він розрахований на роботу на коротких дистанціях (в межах пристрою, як UART та I2C). Працює на швидкостях 1MHz - 200MHz, в залежності від частоти таймера мікроконтролера. SPI-зв’язок зазвичай використовується для взаємодії з такими пристроями, як пам’ять (SD-карти, EEPROM, Flash), дисплеї (OLED), датчики, а також АЦП/ЦАП.

Схема підключення пристроїв SPI

Для зв'язку використовується 4 лінії (дроти):

  • MOSI (Master OUT, Slave IN) - лінія передачі даних від ведучого пристрою до веденого.
  • MISO (Master IN, Slave OUT) - лінія передачі даних від веденого до ведучого.
  • SCK (Serial Clock) - лінія сигналу синхронізації, генерується ведучим.
  • CS/SS (Chip Select або Slave Select) - лінія вибору (активації) конкретного веденого пристрою.

На відміну від I2C, SPI не використовує адресацію для підтримки декількох ведених пристроїв. Натомість використовуються окремі лінії CS/SS — по одній для кожного пристрою.

Ведучий пристрій тримає всі лінії CS/SS у високому рівні (неактивними) і знижує рівень на потрібній лінії, щоб активувати відповідний ведений пристрій.

Режими SPI

SPI має 4 режими роботи, які визначаються двома параметрами:

  • CPOL (Clock Polarity) — рівень тактового сигналу (SCK) у стані спокою.
  • CPHA (Clock Phase) — на якому фронті сигналу зчитуються дані.

Комбінація цих параметрів утворює 4 режими:

Режим CPOL CPHA
Mode 0 0 (станом спокою SCK вважається LOW) 0 (дані зчитуються в момент зміни сигналу SCK з LOW на HIGH)
Mode 1 0 (станом спокою SCK вважається LOW) 1 (дані зчитуються в момент зміни сигналу SCK з HIGH на LOW)
Mode 2 1 (станом спокою SCK вважається HIGH) 0 (дані зчитуються в момент зміни сигналу SCK з LOW на HIGH)
Mode 3 1 (станом спокою SCK вважається HIGH) 1 (дані зчитуються в момент зміни сигналу SCK з HIGH на LOW)


Дуже важливо!
Режим SPI має збігатися як у ведучого (Master), так і у веденого (Slave) пристрою. Якщо вони працюють у різних режимах, дані будуть зчитуватись у неправильний момент — у результаті будуть отримані некоректні значення або взагалі не буде відповіді.

На практиці все просто: кожен SPI-пристрій (датчик, дисплей, пам’ять) має в документації вказаний параметр SPI Mode (0–3). Потрібно лише встановити цей самий режим в мікроконтролері. Загалом, якщо після підключення пристрій не працює, перше, що варто перевірити — чи правильно встановлений режим SPI.

Передача даних

Ведучий пристрій (Master) встановлює лінію SS/CS у низький рівень (LOW), щоб розпочати обмін даними з конкретним веденим пристроєм (Slave).

SPI є повнодуплексним протоколом, тому і ведучий, і ведений пристрої одночасно передають і приймають дані побітово, синхронізуючись із тактовим сигналом.

Якщо ведучий пристрій хоче лише отримати дані від веденого, він повинен надсилати фіктивні (dummy) дані, щоб згенерувати відповідь.

Передача даних у SPI може відбуватись у двох напрямках: від старшого біта до молодшого (MSB → LSB) або від молодшого до старшого (LSB → MSB).

Робота з SPI в Arduino

Фреймворк Arduino має бібліотеку SPI.h для роботи з інтерфейсом SPI, яка має такі функції:

Функції Опис
SPI.begin() Ініціалізує шину SPI, встановлюючи SCK, MOSI як виходи.
SPI.beginTransaction(settings) Ініціалізує шину SPI. Зверніть увагу, що виклик SPI.begin() є обов’язковим перед викликом цього методу.
SPISettings(speedMaximum, dataOrder, dataMode) Об’єкт SPISettings використовується з SPI.beginTransaction() для налаштування SPI.
byte received = SPI.transfer(sentByte); Передає байт і одночасно приймає байт по SPI.
SPI.endTransaction() Припиняє використання шини SPI.
 
Наприклад, візьмемо три плати Arduino Nano: одна буде ведучою, дві інші — веденими. Апаратний SPI використовує піни D11 (MOSI), D12 (MISO) та D13 (SCK). Для вибору пристроїв ведучий використовує окремі GPIO як лінії SS/CS (по одній на кожен пристрій), при цьому пін D10 має бути налаштований як вихід. На ведених пристроях пін D10 використовується як вхід SS.
Схема SPI на Arduino Nano з декількома веденими

Код ведучого пристрою:

#include <SPI.h>

const int SS1 = 10;
const int SS2 = 9;

void setup() {
  SPI.begin(); // запускаємо SPI як master

  // SS піни керують вибором slave
  pinMode(SS1, OUTPUT);
  pinMode(SS2, OUTPUT);

  // Вимикаємо всі slave (HIGH = неактивний)
  digitalWrite(SS1, HIGH);
  digitalWrite(SS2, HIGH);

  //Запускаємо Serial Monitor для виводу в консоль
  Serial.begin(9600);
}

void loop() {
  // Обмін зі slave 1
  digitalWrite(SS1, LOW);  // активуємо перший slave

  // SPI.transfer:
  // - відправляє байт
  // - одночасно читає байт у відповідь
  byte response = SPI.transfer(0xA1);

  digitalWrite(SS1, HIGH); // деактивуємо slave

  Serial.print("Відповідь від Slave1: ");
  Serial.println(response);

  delay(500);

  // Обмін зі slave 2
  digitalWrite(SS2, LOW);  // активуємо другий slave
  response = SPI.transfer(0xA1);
  digitalWrite(SS2, HIGH); // деактивуємо slave

  Serial.print("Відповідь від Slave2: ");
  Serial.println(response);

  delay(500);
}

Код веденого:

#include <SPI.h>

// Останні отримані дані від master
volatile byte received = 0;

// Дані, які slave буде повертати master
volatile byte toSend = 0x11;

void setup() {
  pinMode(MISO, OUTPUT); // лінія передачі даних назад master

  SPCR |= _BV(SPE);      // вмикаємо SPI у режимі slave
  SPI.attachInterrupt(); // дозволяємо обробку через переривання
}

// Викликається автоматично при кожному байті SPI
ISR(SPI_STC_vect) {
  received = SPDR; // читаємо що прийшло від master з регістру

  SPDR = toSend;   // готуємо відповідь для master
}

void loop() {
  // просто змінюємо дані, які віддаємо
  toSend++; // кожен цикл буде інше значення
}