Данная статья является кратким дискурсом по шине SPI и не должна восприниматься как точная техническая документация. Рассматривается только полнодуплексный вариант применения.
Общие сведения:
SPI - (Serial Peripheral Interface) эспиай, последовательный периферийный интерфейс иногда называемый 4-х проводным интерфейсом, является последовательным синхронным интерфейсом передачи данных. Изобретён компанией Motorola в середине 1980-x. В отличие от I2C и UART, SPI требует больше сигналов для работы, но может работать на более высоких скоростях. Не поддерживает адресацию, вместо этого используется сигнал SS (slave select - выбор ведомого), который также иногда называется CS (chip select), CE (chip enable) или SE (slave enable). Поддерживает только одного ведущего на шине. Ведущий устанавливает скорость обмена данными и другие параметры, такие как полярность и фаза тактирования. Обмен данными происходит в режиме полного дуплекса, что означает устройства на шине могут одновременно передавать и принимать данные. Интерфейс использует следующие сигналы (в номенклатуре AVR, для получения точного названия сигналов обратитесь к технической документации микросхемы, с которой работаете):
- MISO (master in slave out) - вход ведущего, выход ведомого
- MOSI (master out slave in) - выход ведущего, вход ведомого
- SCK (serial clock) - сигнал тактирования
- SS (slave select) - сигнал выбор ведомого.
Несмотря на то, что интерфейс называется 4-х проводным, для подключения нескольких ведомых понадобится по одному проводу SS для каждого ведомого (в полнодуплексной реализации). Сигналы MISO, MOSI и SCK являются общими для всех устройств на шине. Ведущий посылает сигнал SS для того ведомого, обмен данными с которым будет осуществляться. Простыми словами, все ведомые, кроме выбранного ведущим будут игнорировать данные на шине. SS является инверсным (active-low), что означает что ведущему необходимо прижать эту линию для выбора ведомого.
Подключение:
SPI на Arduino:
Arduino UNO/Piranha UNO/Arduino ULTRA
На Arduino UNO/Piranha UNO/Arduino ULTRA выводы аппаратного SPI расположены на 10, 11, 12 и 13 выводах, а так же эти выводы соединены с колодкой ICSP (in circuit serial programmer):
Сигнал | Вывод |
---|---|
SS | 10 |
MOSI | 11 |
MISO | 12 |
SCK | 13 |
Arduino MEGA
На Arduino MEGA выводы аппаратного SPI расположены на 50, 51, 52 и 53 выводах, а так же эти выводы соединены с колодкой ICSP (in circuit serial programmer):
Сигнал | Вывод |
---|---|
SS | 53 |
MOSI | 51 |
MISO | 50 |
SCK | 52 |
Пример для Arduino
В этих примерах мы соединим две Arduino по SPI по следующей схеме:
В одну плату необходимо загрузить скетч ведущего, а в другую скетч ведомого. Для проверки работы необходимо открыть проследовательный монитор той платы, в которую загружен скетч ведомого.
Arduino UNO в качестве ведущего:
#include "SPI.h" // Создаём переменную байтового массива byte data[] {'H','e','l','l','o'}; // Создаём переменную строки в формате языка Си char* cstring = " World!"; // создаём переменную с числом 168496141 в шестнадцатеричной системе счисления long i = 0x0A0B0C0D; void setup() { // Инициируем интерфейс SPI в режиме ведущего SPI.begin(); // Устанавливаем логическую "1" на выводе Slave Select pinMode(SS, HIGH); } void loop() { // Прижимаем вывод Slave Select (устанавливаем логический "0"), // тем самым активируя передачу данных с подключенным // к этому выводу ведомым digitalWrite(SS, LOW); // Начинаем передачу данных, передавая функции объект настроек шины // SPISettings( Скорость в Гц, Порядок передачи битов, Режим шины) SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0)); // Передаём массив data for (int i = 0; i < sizeof(data); i++) { SPI.transfer(char(data[i])); } // Передаём один байт SPI.transfer(','); // Передаём строку в формате си for (char* p = cstring; char c = *p; p++) { SPI.transfer(c); } // Передаём один байт - заголовок начала целого числа // Как пример: байт 0xAD обозначает заголовок целого числа типа long SPI.transfer(0xAD); // Побайтово передаём целое число for (int j = 0; j < sizeof(i); j++) { byte b = i >> 8 * j; SPI.transfer(b); } // Передаём байт конца пакета // Как пример: байт 0xAF обозначает конец пакета SPI.transfer(0xAF); // Завершаем передачу данных SPI.endTransaction(); // Завершаем работу с ведомым digitalWrite(SS, HIGH); delay(1000); }
Arduino UNO в качестве ведомого:
#include "SPI.h" // Определяем размер буфера #define BUFFER_SIZE 100 // Создаём буфер для данных SPI char buff[BUFFER_SIZE]; // Создаём переменную индекса буфера // volatile - указания для препроцессора о том, // что переменная может измениться при прерывании // и с ней не стоит производить никаких оптимизаций // при компилировании volatile uint8_t index = 0; // Создаём флаг готовности данных volatile bool data_ready = false; // Создаём переменную для полученного целого числа long i; // Создаём индекс заголовка целого числа volatile uint8_t int_index = 0; void setup (void) { // Инициируем работу с последовательным портом Serial.begin(9600); // Устанавливаем вывод MISO в режим выхода pinMode(MISO, OUTPUT); // Устанавливаем режим ведомого в контрольном регистре SPI (SPI Control Register) SPCR |= _BV(SPE); // Подключаем прерывание SPI SPI.attachInterrupt(); } // Вызываем функцию обработки прерываний по вектору SPI // STC - Serial Transfer Comlete ISR(SPI_STC_vect) { // Получаем байт из регистра данных SPI byte c = SPDR; // Добавляем байт в буфер if (index < sizeof(buff)) { buff[index++] = c; // Как пример: байт 0xAD обозначает заголовок целого числа типа long if (c == 0xAD) // Записываем положение целого числа в байтовом массиве int_index = index; // Как пример: байт 0xAF обозначает конец пакета if (c == 0xAF) // Устанавливаем флаг готовности данных для обработки data_ready = true; } } void loop(void) { // Если установлен флаг готовых данных if (data_ready == true) { // Обнуляем индекс index = 0; // Создаём строку и записываем в неё полученный буфер String message = String(buff); // Форматируем строку, убирая из неё заголовок // целого числа и всё после заголовка message = message.substring(0, int_index - 1); // Выводим отформатированную строку в последовательный порт Serial.println(message); // Обнуляем переменную для хранения полученного целого числа i = 0; // Записываем целое число через указатель на элемент массива /* Пояснение: * *(выражение) - разыменовывание указателя * (long *) - приводим последующее выражение к типу указателя на long * buff+int_index - прибавляем к указателю на первый элемент * массива buff индекс следующего после заголовка элемента массива, */ тем самым получая указатель на наше целое число i = *((long *)(buff + int_index)); // обнуляем флаг готовности данных data_ready = false; Serial.print("long integer over SPI is: "); // Выводим целое число в последовательный порт Serial.println(i); } }
После соединения двух Arduino по SPI и загрузки соответствующих скетчей, мы будем получать следующее сообщение в мониторе последовательного порта ведомого микроконтроллера раз в секунду:
Hello, World!
long integer over SPI is: 168496141
SPI на Raspberry Pi
На Raspberry Pi выводы аппаратного SPI расположены на выводах GPIO7, GPIO8, GPIO9, GPIO10, GPIO11:
Перед работой с SPI необходимо его включить. Сделать это можно из эмулятора терминала командой sudo raspi-config
-> Interfacing options -> Serial -> No -> Yes -> OK -> Finish или из графической среды в главном меню -> Параметры -> Raspberry Pi Configuration -> Interfaces -> SPI
Подробное описание как это сделать можно посмотреть по ссылке Raspberry Pi, включаем I2C, SPI
Пример работы с SPI на Python:
# Импортируем библиотеку для работы с SPI import spidev # Создаём объeкт spi spi = spidev.SpiDev() # Открываем устройство SPI (/dev/spidev0.0) spi.open(0, 0) # Ограничиваем скорость до 1 МГц spi.max_speed_hz = 1000000 # Выводим байтовую строку spi.writebytes(b'Hello, World!') # Выводим заголовок целого числа spi.writebytes([0xAD]) # Создаём целое число в шестнадцатеричной системе счисления i = 0x0A0B0C0D # Конвертируем число в список байтов, # указывая формат little endian b = i.to_bytes(4, byteorder='little') # Передаём число по SPI spi.writebytes(b) # Передаём байт конца пакета spi.writebytes([0xAF])
В отличие от Arduino для Raspberry не существует простых решений для работы в режиме ведомого. Подробней ознакомиться с работой чипа BCM Raspberry можно в технической документации на официальном сайте, стр. 160.
Для проверки работы сценария можно подключить Raspberry по SPI к Arduino со скетчем из примера выше через преобразователь уровней или Trema+Expander Hat:
Подробнее о SPI
Параметры
Существуют четыре режима работы SPI, зависящие от полярности (CPOL) и фазы (CPHA) тактирования:
Режим | Полярность | Фаза | Фронт тактирования | Фронт установки бита данных |
---|---|---|---|---|
SPI_MODE0 | 0 | 0 | Спадающий | Нарастающий |
SPI_MODE1 | 0 | 1 | Нарастающий | Спадающий |
SPI_MODE2 | 1 | 0 | Нарастающий | Спадающий |
SPI_MODE3 | 1 | 1 | Спадающий | Нарастающий |
В Arduino IDE для установки режима необходимо передать функции, возвращающей объект настроек параметр режима работы SPI_MODE, например:
SPISettings(1000000, MSBFIRST, SPI_MODE0)
Для выбора режима работы SPI на Raspberry Pi необходимо вызвать дескриптор объекта SpiDev().mode и присвоить ему битовые значения CPOL и CPHA, например:
# 0b11 соответствует режиму MODE3 spi.mode = 0b11
Скорость передачи данных
Скорость передачи данных устанавливается ведущим и может меняться "на лету". Программист в силах указать лишь максимальную скорость передачи данных.
Обсуждение