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 — на каком фронте (вспышке) тактового сигнала брать данные
Режим | CPOL | CPHA | SCK в покое | Когда считываем данные |
---|---|---|---|---|
0 | 0 | 0 | Низкий | На переднем фронте (0→1) |
1 | 0 | 1 | Низкий | На заднем фронте (1→0) |
2 | 1 | 0 | Высокий | На переднем фронте (1→0) |
3 | 1 | 1 | Высокий | На заднем фронте (0→1) |
Важно: и 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. Без волшебства. Только голое железо и биты 🧵