View Categories

Интерфейс SPI

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

  • Синхронный (есть тактовая линия)
  • Последовательный (данные передаются бит за битом по одному проводу)
  • Количество пинов: от 2 до 4
  • Скорость: зависит от устройства и качества линии, от 0 Гц до 150 МГц
  • Топология: одно ведущее устройство
  • Количество ведомых на шине: не ограничено

Основы #

Роли #

SPI является шиной, т.е. к одним и тем же выводам может быть подключено несколько устройств. На шине есть только одно ведущее устройство - Master, остальные - ведомые, Slave. Ведущее выбирает, с каким из ведомых взаимодействовать, может отправлять и читать с него данные. Ведущим обычно является основной МК в схеме, а остальные - различные цифровые микросхемы, датчики или вспомогательные МК.

В современной литературе используется другая терминология: Controller и Peripheral. Уход от классических Master (хозяин) и Slave (раб) был связан со слишком близкой аналогией с работой на плантациях

Пины #

SPI использует максимум 4 пина для подключения. Рассмотрим их функции:

  • MOSI (Master Out Slave In) - ведущее устройство отправляет данные, ведомое принимает. На "модуле" может быть подписан как SDI, DI, DIN, SI
  • MISO (Master In Slave Out) - ведомое устройство отправляет данные, ведущее принимает. На "модуле" может быть подписан как SDO, DO, DON, SO
  • SS (Slave Select) - выбор ведомого устройства, сигналом управляет мастер. На "модуле" может быть подписан как CS (Chip Select)
  • SCK (Serial Clock) - тактирование (клок, импульсы синхронизации), сигналом управляет мастер. На "модуле" может быть подписан как SCLK, CLK, SPC

"Режимы работы" пинов на устройствах:

Пин Master Slave
MOSI OUTPUT INPUT
MISO INPUT OUTPUT
SS OUTPUT INPUT
SCK OUTPUT INPUT

Подключение #

SPI - очень примитивный интерфейс, поэтому может подключаться на любые цифровые пины МК и работать в "ручном режиме" или при помощи функций типа shiftIn() и shiftOut() - опять же это программный SPI. Многие МК имеют и аппаратный SPI, который обычно в разы быстрее программной реализации, а также может отправлять данные асинхронно.

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

На модуле или микросхеме может быть неполный набор пинов, но их будет как минимум два - дата (MOSI или MISO) и клок (SCK), если микросхема передаёт данные только в одну сторону.

На некоторых микросхемах и китайских модулях отсутствует пин CS - такие модули нельзя подключать на одни пины (MISO-MOSI-SCK) с остальными - они не смогут корректно работать, т.к. "активны" по умолчанию и будут реагировать на передачи для соседей

Расположение аппаратных выводов SPI микроконтроллера или отладочной платы всегда указано на распиновке. Рассмотрим подключение нескольких условных модулей к аппаратному SPI на Arduino Nano:

На распиновке можно увидеть также пин SS - на тот случай, если Nano будет являться ведомым устройством

Здесь есть все три варианта:

  • DEVICE1 - только принимает данные
  • DEVICE2 - только отправляет данные
  • DEVICE3 - принимает и отправляет данные

Процесс передачи #

Процесс передачи начинается с выбора активного ведомого устройства - для этого его пин SS обычно подключается к GND (подаётся низкий сигнал, LOW). То есть в остальное время на пине должен быть высокий сигнал, начиная с запуска МК.

После выбора начинается передача: на дата-пинах появляется логический уровень, соответствующий передаваемому биту. После этого мастер меняет сигнал на пине SCK - ведомое устройство видит его и читает данные от мастера на пине MOSI, а мастер читает данные с пина MISO. Сигнал на SCK снова меняется, в это время устройства выводят на свои дата-пины следующий бит.

Процесс повторяется до тех пор, пока мастер не "отпустит" SS. Сколько бит будет передано или получено - зависит от конкретного устройства и программы на мастере, но в большинстве случаев кратно 8, т.е. идёт обмен байтами.

Таким образом, передача идёт одновременно в обе стороны. На практике такое встречается редко, обычно устройство или читает, или отвечает, даже если имеет оба дата-пина

Режимы SPI #

На самом деле всё чуть сложнее, чем написано выше - есть 4 варианта, что считать за "изменение сигнала" на пине SCK: начальный уровень клока (Clock Polarity, CPOL), а также работа по его восходящему или нисходящему фронту (Clock Phase, CPHA). Режимы имеют общепринятые номера от 0 до 3, в Arduino это константы SPI_MODEn:

Режим CPOL CPHA Вывод данных Чтение
SPI_MODE0 0 0 SCK ↓ или SS SCK ↑
SPI_MODE1 0 1 SCK ↑ SCK ↓
SPI_MODE2 1 0 SCK ↑ или SS SCK ↓
SPI_MODE3 1 1 SCK ↓ SCK ↑

Самым распространённым является первый режим - SPI_MODE0, а иногда устройства поддерживают сразу SPI_MODE0 и SPI_MODE3

Arduino Shift #

Полное описание смотри в справочнике

В фреймворке Arduino есть две функции, которые позволяют отправить и прочитать байт данных при помощи программного SPI, т.е. с любых цифровых пинов. Это удобно, когда не нужна высокая скорость передачи, неудобно использовать пины аппаратной шины или устройство не имеет пина CS и не может работать на основной шине вместе с остальными:

  • uint8_t shiftIn(uint8_t dataPin, uint8_t clockPin, uint8_t bitOrder) - читает и возвращает 1 байт (8 бит) с дата-пина dataPin и клок-пина clockPin в порядке bitOrder
  • void shiftOut(uint8_t dataPin, uint8_t clockPin, uint8_t bitOrder, uint8_t val) - отправляет 1 байт (8 бит) значение val с дата-пина dataPin и клок-пина clockPin в порядке bitOrder

Где bitOrder:

  • MSBFIRST - сначала старший бит
  • LSBFIRST - сначала младший бит

В большинстве устройств используется порядок MSBFIRST, уточнить можно в документации на конкретную микросхему

Обе функции работают в режиме SPI_MODE0, он не настраивается

Пример отправки #

В качестве примера возьмём сдвиговый регистр 74HC595, он позволит буквально увидеть передаваемые данные при помощи светодиодов.

В наборе GyverKIT START IOT EXTRA
Сдвиговый регистр

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

Вот программа, которая готовит пины к работе и отправляет биты 0b11001111 в чип, а он в свою очередь выводит их как напряжение на свои пины. Мы подключили к ним светодиоды, так что "увидим" переданные данные:

#define CS_595 10
#define DAT_595 11
#define CLK_595 13

void sendByte(uint8_t data) {
  digitalWrite(CS_595, LOW);                    // выбрать активным
  shiftOut(DAT_595, CLK_595, MSBFIRST, data);   // отправить
  digitalWrite(CS_595, HIGH);                   // деактивировать
}

void setup() {
  // пины как выходы
  pinMode(CS_595, OUTPUT);
  pinMode(DAT_595, OUTPUT);
  pinMode(CLK_595, OUTPUT);

  // деактивировать
  digitalWrite(CS_595, HIGH);

  delay(100);

  // отправить
  sendByte(0b11001111);
}

void loop() {
}

Результат:

Если сменить режим на LSBFIRST, то светодиоды загорятся в обратном порядке:

Вот реальный сигнал с пинов, снятый при помощи логического анализатора. Данные передавались в режиме MSBFIRST:


Сверху вниз CS, MOSI, CLK

Скорость #

Частота тактирования (и передачи данных) у этих функций по моим тестам следующая:

Платформа Частота, MHz
AVR 0.06
ESP8266 0.2
ESP32 0.96
ESP32C3 0.35

Что в десятки раз ниже скорости аппаратного SPI на этих же платформах, например на Arduino Nano можно передавать с частотой до 8 MHz!

В моей библиотеке GyverIO есть ускоренные версии этих функций для всех платформ. Если вам нужен быстрый программный SPI - используйте её. Например, на Arduino Nano частота составляет 1.3 MHz, что в 22 раза быстрее стандартных функций

Библиотека SPI.h #

Полное описание смотри в справочнике

В фреймворке Arduino есть инструмент для работы с аппаратным SPI - библиотека SPI.h, она позволяет отправлять данные с более высокой скоростью, а также с возможностью настроить её и остальные параметры шины. Рассмотрим минимальный пример:

#include <SPI.h>

void setup() {
  // запустить шину
  SPI.begin();

  // активировать CS ведомого

  // начать передачу с параметрами 1 МГц, MSBFIRST, SPI_MODE0
  SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));

  // передать байты
  SPI.transfer(123);
  SPI.transfer(0b10100101);

  // массив тоже можно
  uint8_t data[] = {0xab, 0xcd};
  SPI.transfer(data, 2);

  // закончить передачу
  SPI.endTransaction();

  // деактивировать CS ведомого

  // остановить шину (необязательно)
  SPI.end();
}
void loop() {}

Методы beginTransaction() и endTransaction() позволяют нескольким SPI-устройствам безопасно использовать шину с разными настройками

Примечание (есть в справочнике, но продублирую здесь):

  • Вызов SPI.begin() переводит пины шины SCK и MOSI в режим OUTPUT. При запущенной шине пины не подчиняются функции digitalWrite()
  • Вызов SPI.end() не меняет режим пинов шины
  • Если все три параметра SPISettings - константы, то эффективнее вызывать начало передачи сразу с конструктором настроек: SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));, как в примере выше
  • Если хотя бы один - не константа, то эффективнее создать объект SPISettings отдельно и передать его в beginTransaction():
uint32_t freq;  // = ...
SPISettings spiset(freq, MSBFIRST, SPI_MODE0);

SPI.beginTransaction(spiset);

Чтение #

Для чтения данных нужно получить их как результат функции transfer(uint8_t), т.е. для чтения нужно в любом случае вызвать передачу с любыми данными. Например uint8_t read = SPI.transfer(0); - мы передаём устройству 0 (оно его игнорирует, если мы знаем что делаем), а в ответ получаем от него байт данных или 0, если оно ничего не ответило.

Второй вариант - при отправке массива transfer(uint8_t*, size_t) библиотека записывает в него ответ устройства, т.е. перезаписывает наши данные! Бывают ситуации, когда они нам всё ещё нужны, например буфер кадра дисплея. Даже если устройство ничего не ответит, если провод не подключен - массив заполнится нулями. А если мы ожидаем ответ - то нужными данными: сразу после вызова transfer они будут доступны для использования.

Пример отправки #

Отправим тот же байт в ту же схему с 74HC595 и светодиодами, на этот раз при помощи аппаратного SPI. Результат на светодиодах будет таким же:

#include <Arduino.h>
#include <SPI.h>

#define CS_595 10   // пин CS, остальные на шине

void sendByte(uint8_t data) {
  digitalWrite(CS_595, LOW);    // выбрать активным
  SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); // начать
  SPI.transfer(data);           // отправить
  SPI.endTransaction();         // закончить отправку
  digitalWrite(CS_595, HIGH);   // деактивировать
}

void setup() {
  // запустить шину
  SPI.begin();

  // пин CS как выход
  pinMode(CS_595, OUTPUT);

  // деактивировать
  digitalWrite(CS_595, HIGH);

  delay(100);

  sendByte(0b11001111);
}

void loop() {
}

Снимок с логического анализатора. Обратите внимание на время передачи, как оно уменьшилось по сравнению с shiftOut():

Как можно заметить, скорость очень сильно выросла - теперь её даже ограничивает медленный digitalWrite() на пине CS. Заменим его на быстрый аналог из GyverIO:

Гораздо лучше, теперь байт передался за 1.6 микросекунды!

Скорость #

Нужно быть аккуратнее с выбором частоты тактирования шины:

  • Чем длиннее провода и хуже подключение, тем ниже должна быть скорость. Для подключения на макетной плате лучше не использовать выше 1 МГц
  • Нижний порог по скорости обычно отсутствует, поэтому допустимый диапазон скоростей очень большой и можно пользоваться функциями типа shiftOut(), которые сделаны буквально "на коленке"
  • Максимальная частота для устройства обычно указана в документации к нему. Например, тот же 74HC595 может работать на 25 MHz при питании от 4.5V и на 5 MHz от 2V. У драйвера матриц и дисплеев MAX7219 в документации указан минимальный CLK Clock Period 100 ns (наносекунд), т.е. максимальная частота - 10 МГц

Полезные страницы #

Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Прокрутить вверх