Интерфейс SPI (Serial Peripheral Interface) широко используется для связи микроконтроллеров с датчиками, памятью и другими устройствами. В большинстве проектов ESP32 выступает в роли мастера (Master), но иногда требуется настроить его как ведомое устройство (Slave) — например, для приёма данных от другого контроллера или специализированного чипа. В этом режиме ESP32 не инициирует обмен, а только реагирует на команды мастера, что открывает новые возможности для построения распределённых систем.

В отличие от режима Master, где ESP32 полностью контролирует шину, настройка Slave SPI имеет ряд нюансов: от выбора аппаратных выводов до обработки прерываний для своевременного чтения данных. В этой статье мы разберём уникальную особенность ESP32 — поддержку до 4-х независимых SPI-интерфейсов (SPI0–SPI3), где только SPI2 и SPI3 могут работать в режиме Slave, а также рассмотрим практические примеры для Arduino IDE и ESP-IDF.

Что такое режим Slave SPI и когда он нужен

В режиме Slave SPI микроконтроллер ESP32 не генерирует сигнал тактирования (SCK), а синхронизируется по внешнему источнику — мастеру. Основные особенности этого режима:

  • 🔄 Пассивная роль: ESP32 только отвечает на запросы мастера, не может инициализировать передачу самостоятельно.
  • 📡 Приём данных по прерыванию: для надёжной работы требуется обработчик прерываний (ISR), иначе данные могут быть утеряны.
  • Ограниченная скорость: максимальная частота зависит от тактовой частоты ESP32 и настройки делителя (до 80 МГц в теории, но на практике редко превышает 20 МГц).
  • 🔌 Аппаратные ограничения: не все выводы ESP32 поддерживают Slave SPI (например, SPI1 может работать только как Master).

Типичные сценарии использования:

  • 📊 Сбор данных с нескольких датчиков, подключённых к мастер-устройству (например, Raspberry Pi или STM32).
  • 🔗 Реализация протоколов типа CAN или 1-Wire через SPI-мосты.
  • 🖥️ Обмен данными с FPGA или специализированными чипами (например, AD9959 для генерации сигналов).
  • 🔄 Дуплексная связь между двумя ESP32, где одно устройство всегда мастер, а другое — ведомое.

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

📊 Для чего вы используете SPI на ESP32?
  • Подключение датчиков
  • Связь с другим микроконтроллером
  • Работа с памятью (Flash/SRAM)
  • Управление дисплеями
  • Другое

Аппаратные возможности ESP32 для Slave SPI

ESP32 имеет 4 SPI-контроллера (SPI0SPI3), но не все из них поддерживают режим Slave. В таблице ниже приведены ключевые характеристики:

Контроллер Режим Slave Макс. частота (МГц) Поддерживаемые выводы Особенности
SPI0 ❌ Нет Используется для flash-памяти, недоступен для пользователя.
SPI1 ❌ Нет до 80 HSPI Только Master, часто используется для связи с дисплеями.
SPI2 ✅ Да до 80 VSPI Поддерживает Slave, но требует ручной настройки выводов.
SPI3 ✅ Да до 80 HSPI (альтернативные выводы) Рекомендуется для Slave, так как имеет выделенные сигналы SS.

Для режима Slave оптимально использовать SPI2 или SPI3. При этом:

  • 🔌 Вывод SS (Slave Select) обязателен — без него ESP32 не сможет определить начало передачи.
  • 🔄 Выводы MISO (Master In Slave Out) и MOSI (Master Out Slave In) могут быть переназначены, но SCK должен быть жёстко привязан к определённому GPIO.
  • ⚡ Для стабильной работы рекомендуется использовать pull-up резисторы на линиях MOSI и SCK (10 кОм).
⚠️ Внимание: Если вы используете SPI3 в режиме Slave, убедитесь, что в настройках menuconfig (для ESP-IDF) отключён параметр CONFIG_SPI_MASTER_ISR_IN_IRAM. В противном случае возможны конфликты с обработчиками прерываний.

Схема подключения ESP32 в режиме Slave SPI

Типовая схема подключения ESP32 как ведомого устройства включает 4 основных сигнала:

  • SCK (Serial Clock) — тактовый сигнал от мастера.
  • MOSI (Master Out Slave In) — данные от мастера к ведомому.
  • MISO (Master In Slave Out) — данные от ведомого к мастеру.
  • SS (Slave Select) — сигнал выбора ведомого (активный низкий уровень).

Пример подключения ESP32 (Slave) к Arduino Uno (Master):


Arduino Uno (Master) ↔ ESP32 (Slave)

------------------- ---------------

5V → VCC (5V или 3.3V с делителем)

GND → GND

D13 (SCK) → GPIO 18 (SCK)

D11 (MOSI)→ GPIO 23 (MOSI)

D12 (MISO)← GPIO 19 (MISO)

D10 (SS) → GPIO 5 (SS)

Ключевые моменты:

  • 🔋 Уровни напряжения: Если мастер работает на 5V, а ESP32 — на 3.3V, используйте делитель напряжения или логический конвертер для линий MOSI и SCK.
  • 🔌 Pull-up резисторы: Рекомендуется установить резисторы 10 кОм на линии SCK и SS для предотвращения ложных срабатываний.
  • Длина проводов: Для частот выше 10 МГц используйте короткие провода (менее 10 см) или экранную витую пару.

Выводы SCK, MOSI, MISO подключены корректно|

Сигнал SS подключён к GPIO с поддержкой ввода|

Уровни напряжения совместимы (3.3V/5V)|

Установлены pull-up резисторы на SCK и SS|

Питание ESP32 стабильно (не менее 500 мА)-->

Настройка Slave SPI в Arduino IDE

Для работы с Slave SPI в Arduino IDE используется библиотека SPI, но с специфическими настройками. Основные шаги:

  1. Инициализация SPI в режиме Slave:
    #include <SPI.h>
    
    

    void setup() {

    SPI.begin(SPI_MODE_SLAVE); // Инициализация в режиме Slave

    pinMode(SS, INPUT_PULLUP); // Настройка вывода SS как входа с pull-up

    }

  2. Обработка прерывания: Данные приходят по прерыванию, поэтому нужно использовать SPI.attachInterrupt():
    void setup() {
    

    SPI.begin(SPI_MODE_SLAVE);

    SPI.attachInterrupt(); // Включение прерывания

    }

    // Обработчик прерывания

    ISR(SPI_STC_vect) {

    byte received = SPDR; // Чтение принятого байта

    // Обработка данных

    }

  3. Отправка данных мастеру: Для этого используется регистр SPDR:
    void loop() {
    

    if (digitalRead(SS) == LOW) { // Если Slave выбран

    SPDR = 0xAA; // Отправка байта мастеру

    while (!(SPSR & (1 << SPIF))); // Ожидание завершения передачи

    }

    }

Пример полного кода для приёма данных от мастера и отправки ответа:

#include <SPI.h>

volatile bool dataReceived = false;

volatile byte receivedData;

void setup() {

Serial.begin(115200);

pinMode(SS, INPUT_PULLUP);

SPI.begin(SPI_MODE_SLAVE);

SPI.attachInterrupt();

}

ISR(SPI_STC_vect) {

receivedData = SPDR;

dataReceived = true;

}

void loop() {

if (dataReceived) {

Serial.print("Received: 0x");

Serial.println(receivedData, HEX);

SPDR = receivedData + 1; // Отправляем мастеру байт +1

dataReceived = false;

}

}

⚠️ Внимание: В Arduino IDE для ESP32 обработчик прерывания ISR(SPI_STC_vect) может конфликтовать с другими прерываниями. Если данные теряются, попробуйте увеличить приоритет прерывания SPI в esp_intr_alloc (требует перехода на ESP-IDF).
💡

Если мастер не получает ответ от ведомого, проверьте, что сигнал SS удерживается в низком состоянии достаточно долго (не менее 1 такта SCK). Некоторые мастера (например, STM32) могут слишком быстро переключать SS, что приводит к пропуску данных.

Настройка Slave SPI в ESP-IDF

Для профессиональных проектов рекомендуется использовать ESP-IDF — официальный фреймворк от Espressif. Здесь настройка Slave SPI гибче, но требует больше кода.

Основные шаги:

  1. Конфигурация SPI: В файле sdkconfig убедитесь, что включена поддержка SPI:
    CONFIG_SPI_MASTER_ISR_IN_IRAM=n
    

    CONFIG_SPI_SLAVE_ISR_IN_IRAM=y

  2. Инициализация шины:
    #include "driver/spi_slave.h"
    
    

    spi_bus_config_t buscfg = {

    .miso_io_num = GPIO_NUM_19, // MISO

    .mosi_io_num = GPIO_NUM_23, // MOSI

    .sclk_io_num = GPIO_NUM_18, // SCK

    .quadwp_io_num = -1,

    .quadhd_io_num = -1,

    .max_transfer_sz = 32, // Макс. размер передачи (байт)

    };

    spi_slave_interface_config_t slvcfg = {

    .spics_io_num = GPIO_NUM_5, // SS

    .flags = 0,

    .queue_size = 7,

    .mode = 0, // SPI_MODE0

    .post_setup_cb = NULL,

    .post_trans_cb = NULL

    };

  3. Создание устройства Slave:
    spi_device_handle_t spi;
    

    spi_slave_initialize(SPI2_HOST, &buscfg, &slvcfg, &spi);

  4. Приём и отправка данных:
    uint8_t sendBuf[32] = {0};
    

    uint8_t recvBuf[32] = {0};

    spi_slave_transaction_t t = {

    .length = 32 * 8, // Размер в битах

    .tx_buffer = sendBuf,

    .rx_buffer = recvBuf

    };

    spi_slave_transmit(spi, &t, portMAX_DELAY);

Полный пример кода для ESP-IDF с обработкой прерываний:

Полный код для ESP-IDF с обработкой прерываний

#include "driver/spi_slave.h"

#include "esp_log.h"

static const char *TAG = "SPI_SLAVE";

void spi_task(void *arg) {

spi_bus_config_t buscfg = {

.miso_io_num = GPIO_NUM_19,

.mosi_io_num = GPIO_NUM_23,

.sclk_io_num = GPIO_NUM_18,

.quadwp_io_num = -1,

.quadhd_io_num = -1,

.max_transfer_sz = 32,

};

spi_slave_interface_config_t slvcfg = {

.spics_io_num = GPIO_NUM_5,

.flags = 0,

.queue_size = 7,

.mode = 0,

};

spi_device_handle_t spi;

spi_slave_initialize(SPI2_HOST, &buscfg, &slvcfg, &spi);

uint8_t sendBuf[32] = {0};

uint8_t recvBuf[32] = {0};

while (1) {

spi_slave_transaction_t t = {

.length = 32 * 8,

.tx_buffer = sendBuf,

.rx_buffer = recvBuf

};

spi_slave_transmit(spi, &t, portMAX_DELAY);

ESP_LOGI(TAG, "Received: %02X %02X %02X...", recvBuf[0], recvBuf[1], recvBuf[2]);

vTaskDelay(10 / portTICK_PERIOD_MS);

}

}

void app_main() {

xTaskCreate(spi_task, "spi_task", 4096, NULL, 5, NULL);

}

Преимущества ESP-IDF перед Arduino IDE:

  • 🔧 Гибкая настройка прерываний: можно задать приоритет и обработчик в IRAM.
  • Поддержка DMA: для высокоскоростного обмена (до 80 МГц).
  • 📊 Отладка через ESP_LOG: удобный вывод логов по UART.
💡

В ESP-IDF для Slave SPI обязательно настройте параметр max_transfer_sz в соответствии с максимальным пакетом данных. Если пакеты больше этого значения, данные будут обрезаны.

Обработка данных и типичные ошибки

При работе с Slave SPI на ESP32 часто возникают проблемы, связанные с синхронизацией, потерями данных или некорректной инициализацией. Рассмотрим типичные ошибки и способы их устранения.

1. Потеря данных при высокой скорости

Если мастер передаёт данные быстрее, чем ESP32 успевает их обработать, возможны потери. Решения:

  • ⚡ Уменьшите частоту SCK на стороне мастера (например, до 1 МГц).
  • 🔄 Используйте DMA в ESP-IDF для буферизации данных.
  • 📡 Увеличьте размер очереди (queue_size) в конфигурации Slave.

2. Slave не отвечает мастеру

Если мастер не получает данные от ESP32, проверьте:

  • 🔌 Корректность подключения MISO (мастер должен читать с этого вывода).
  • ⚡ Уровень сигнала SS: он должен быть низким во время передачи.
  • 🔄 Наличие pull-up резистора на SS (если мастер не удерживает сигнал).

3. Конфликты прерываний

Если данные принимаются с искажениями, возможно, обработчик прерывания SPI конфликтует с другими задачами. Решения:

  • 🔧 В ESP-IDF назначьте прерыванию SPI высший приоритет:
    esp_intr_alloc(SPI_INTR_SOURCE, ESP_INTR_FLAG_IRAM, spi_isr, NULL, &handle);
  • 📵 Отключите другие прерывания (например, Wi-Fi) на время критических операций.
⚠️ Внимание: Если вы используете Wi-Fi или Bluetooth одновременно с Slave SPI, возможны задержки в обработке прерываний из-за конфликтов за доступ к шине APB. В этом случае рекомендуется использовать SPI3 (он работает через отдельную шину AHB).

4. Неправильный режим SPI (MODE0–MODE3)

ESP32 и мастер должны использовать одинаковый режим (CPOL и CPHA). Если данные искажаются, проверьте настройки:

Режим CPOL CPHA Описание
MODE0 0 0 Тактирование по переднему фронту, данные захват по заднему.
MODE1 0 1 Тактирование по заднему фронту, данные захват по переднему.
MODE2 1 0 CPOL=1, CPHA=0.
MODE3 1 1 CPOL=1, CPHA=1.

В Arduino IDE режим задаётся при инициализации:

SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));

В ESP-IDF — в структуре spi_slave_interface_config_t (поле mode).

Практические примеры использования

Рассмотрим два реальных сценария применения Slave SPI на ESP32.

Пример 1: Приём данных от STM32 (Master)

Задача: STM32 отправляет пакеты данных (температура, влажность) по SPI, а ESP32 принимает их и передаёт по Wi-Fi.

Код для ESP32 (Slave):

#include <SPI.h>

#include <WiFi.h>

const char* ssid = "your_SSID";

const char* password = "your_PASSWORD";

volatile uint8_t temp, humidity;

volatile bool newData = false;

void setup() {

Serial.begin(115200);

WiFi.begin(ssid, password);

pinMode(SS, INPUT_PULLUP);

SPI.begin(SPI_MODE_SLAVE);

SPI.attachInterrupt();

}

ISR(SPI_STC_vect) {

static uint8_t index = 0;

uint8_t data = SPDR;

if (index == 0) temp = data;

else if (index == 1) {

humidity = data;

newData = true;

}

index = (index + 1) % 2;

SPDR = 0xFF; // Отправляем мастеру подтверждение

}

void loop() {

if (newData && WiFi.status() == WL_CONNECTED) {

Serial.printf("Temp: %d°C, Humidity: %d%%\n", temp, humidity);

// Отправка данных на сервер...

newData = false;

}

}

Пример 2: Обмен данными между двумя ESP32

Задача: Одна ESP32 (Master) отправляет команды, вторая (Slave) выполняет их и возвращает статус.

Код для Master (ESP32):

#include <SPI.h>

void setup() {

Serial.begin(115200);

SPI.begin();

pinMode(SS, OUTPUT);

digitalWrite(SS, HIGH);

}

void loop() {

digitalWrite(SS, LOW);

uint8_t command = 0x01; // Команда "включить светодиод"

uint8_t response = SPI.transfer(command);

digitalWrite(SS, HIGH);

Serial.print("Sent: 0x");

Serial.print(command, HEX);

Serial.print(", Received: 0x");

Serial.println(response, HEX);

delay(1000);

}

Код для Slave (ESP32):

#include <SPI.h>

const int ledPin = 2;

void setup() {

pinMode(ledPin, OUTPUT);

pinMode(SS, INPUT_PULLUP);

SPI.begin(SPI_MODE_SLAVE);

SPI.attachInterrupt();

}

ISR(SPI_STC_vect) {

uint8_t cmd = SPDR;

if (cmd == 0x01) {

digitalWrite(ledPin, HIGH);

SPDR = 0xAA; // Подтверждение выполнения

} else if (cmd == 0x02) {

digitalWrite(ledPin, LOW);

SPDR = 0x55;

} else {

SPDR = 0xFF; // Ошибка

}

}

void loop() {}

💡

При обмене между двумя ESP32 убедитесь, что мастер удерживает сигнал SS низким на всё время передачи пакета. Если SS будет подниматься между байтами, Slave сбросит состояние.

FAQ: Частые вопросы по Slave SPI на ESP32

Можно ли использовать SPI1 в режиме Slave?

Нет, SPI1 (HSPI) в ESP32 поддерживает только режим Master. Для Slave доступны SPI2 (VSPI) и SPI3 (HSPI с альтернативными выводами).

Почему данные искажаются при частоте выше 10 МГц?

Это связано с задержками в обработке прерываний и длиной проводов. Попробуйте:

  • Уменьшить частоту до 5–10 МГц.
  • Использовать экранные провода.
  • В ESP-IDF включить DMA для SPI.
Как проверить, что Slave получает данные?

Подключите логический анализатор к линиям SCK, MOSI и SS. В Arduino IDE можно выводить принятые данные в Serial:

ISR(SPI_STC_vect) {

byte data = SPDR;

Serial.print("Received: ");

Serial.println(data, HEX);

}

В ESP-IDF используйте ESP_LOGI.

Можно ли использовать Slave SPI одновременно с Wi-Fi?

Да, но возможны задержки в обработке SPI из-за прерываний от Wi-Fi. Рекомендации:

  • Используйте SPI3 (он работает через AHB, что減少 конфликты).
  • Увеличьте приоритет прерывания SPI в ESP-IDF.
  • Отключайте Wi-Fi на время критических операций SPI.
Какие резисторы нужны для подтяжки линий SPI?

Рекомендуется:

  • SCK: 10 кОм pull-up к 3.3V.
  • SS: 10 кОм pull-up к 3.3V (если мастер не удерживает сигнал).
  • MOSI: pull-up не обязателен, но может помочь при длинных проводах.

MISO обычно не требует подтяжки, так как управляется Slave.