КОРЗИНА
магазина
8 (499) 500-14-56 | ПН. - ПТ. 12:00-18:00
ЛЕСНОРЯДСКИЙ ПЕРЕУЛОК, 18С2, БЦ "ДМ-ПРЕСС"

SPI -Serial Peripheral Interface, краткое руководство

Данная статья является кратким дискурсом по шине 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

Скорость передачи данных

Скорость передачи данных устанавливается ведущим и может меняться "на лету". Программист в силах указать лишь максимальную скорость передачи данных.




Обсуждение

Гарантии и возврат Используя сайт Вы соглашаетесь с условями