SPI: как это работает на самом деле

Если ты устал от волшебных команд вроде SPI.transfer() и хочешь разобраться, как устроен SPI «под капотом» — добро пожаловать. Сейчас будет хардкор. Но понятный.

🔩 Внутренности SPI-протокола

SPI — это синхронный последовательный протокол. Это значит, что данные передаются бит за битом под музыку (такт) от ведущего (Master) к ведомому (Slave).

🧷 Четыре основных линии:

  • MOSI — Master Out, Slave In (ведущий отправляет)
  • MISO — Master In, Slave Out (ведомый отвечает)
  • SCK — Serial Clock (такт)
  • SS/CS — Slave Select / Chip Select (выбор ведомого)

Всё просто. Но начинается весёлое, когда в дело вступают параметры CPOL и CPHA.

⏱ CPOL и CPHA — что это и зачем

SPI имеет 4 режима работы. Они определяются двумя параметрами:

  • CPOL — состояние линии такта (SCK) в покое
  • CPHA — на каком фронте (вспышке) тактового сигнала брать данные
РежимCPOLCPHASCK в покоеКогда считываем данные
0 0 0 Низкий На переднем фронте (0→1)
1 0 1 Низкий На заднем фронте (1→0)
2 1 0 Высокий На переднем фронте (1→0)
3 1 1 Высокий На заднем фронте (0→1)

 

ncsinter less4 spi 2

Важно: и Master, и Slave должны быть в одном и том же режиме. Если ты собрал дисплей в режиме 0, а контроллер — в режиме 3 — они не поймут друг друга.

⚙️ Пример настройки SPI вручную (AVR, без Arduino)

Контроллер: ATmega328P
Язык: C (avr-gcc)
Режим: SPI Master, режим 0, скорость F_CPU / 16

#define F_CPU 16000000UL
#include <avr/io.h>
 
void SPI_init(void) {
    // Настраиваем MOSI и SCK как выходы, SS тоже
    DDRB |= (1 << PB3) | (1 << PB5) | (1 << PB2);
 
    // Включаем SPI, Master Mode, режим 0, скорость F_CPU / 16
    SPCR = (1 << SPE)    // Включить SPI
         | (1 << MSTR)   // Master
         | (0 << CPOL)   // CPOL = 0
         | (0 << CPHA)   // CPHA = 0
         | (1 << SPR0);  // Делим частоту на 16
}
 
void SPI_send(uint8_t data) {
    SPDR = data;               // Записали байт
    while (!(SPSR & (1 << SPIF)));  // Ждём окончания передачи
}

Что делает код:

  • Устанавливает выводы MOSI, SCK и SS в режим OUTPUT
  • Включает SPI в режиме Master
  • Устанавливает SPI-режим 0 (CPOL=0, CPHA=0)
  • Функция SPI_send() отправляет 1 байт и ждёт, пока передача завершится

📎 Важное:

  • В SPI **чтение и запись идут одновременно**. Когда ты отправляешь байт, ты одновременно получаешь байт от ведомого.
  • Чтобы просто «прочитать» данные от ведомого, нужно отправить пустой байт: SPI_send(0xFF);

🧪 SPI на STM32 (регистр Level)

Если у тебя STM32 (например, F103), то SPI настраивается через регистры RCC, GPIO и SPI. Вот пример (без HAL, чисто на регистрах):

// Настройка тактирования SPI1
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_SPI1EN;
 
// Настройка выводов: PA5=SCK, PA7=MOSI
GPIOA->CRL = (GPIOA->CRL &~ 0xFF000000)
           | (0xB << (5*4))   // PA5 – альт. функция push-pull
           | (0xB << (7*4));  // PA7 – MOSI
 
// Настройка SPI1
SPI1->CR1 = SPI_CR1_MSTR     // Master mode
          | SPI_CR1_SSM      // Программное управление SS
          | SPI_CR1_SSI
          | SPI_CR1_BR_1     // Делим такт на 8
          | SPI_CR1_SPE;     // Включить SPI

Этот код включает SPI в режиме Master, на частоте F_CPU / 8. Ты можешь установить биты CPOL и CPHA по таблице выше.

🎓 Заключение

  • SPI — не просто 4 провода, а точный танец сигналов
  • Режимы CPOL/CPHA должны совпадать у Master и Slave
  • Arduino прячет всё за SPI.begin(), но если ты хочешь стабильности и скорости — управляй регистрами сам
  • SPI работает быстро, но требует аккуратности: выводы, частота, логические уровни — всё имеет значение

Теперь ты знаешь, как на самом деле работает SPI. Без волшебства. Только голое железо и биты 🧵