Протоколы связи в ESP32-S3 с помощью ESP-IDF (урок 4)


При разработке крупных проектов встраиваемых систем часто возникает необходимость взаимодействия со множеством внешних устройств, таких как датчики, дисплеи, модули памяти и исполнительные механизмы. Однако количество доступных выводов GPIO на микроконтроллере, таком как ESP32-S3, может быть ограничено.

Для преодоления этого ограничения используются протоколы связи. Эти протоколы позволяют микроконтроллеру эффективно взаимодействовать с несколькими устройствами, отправляя и принимая данные по общим линиям связи, вместо того чтобы использовать отдельный вывод для каждого устройства. Это позволяет управлять несколькими периферийными устройствами, минимизируя при этом использование GPIO.

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

ESP32-S3 поддерживает широкий спектр коммуникационных протоколов, включая:

  • UART (универсальный асинхронный приёмопередатчик);
  • SPI (последовательный периферийный интерфейс);
  • I²C (межинтегральная схема);
  • I²S / PDM (протоколы аудиосвязи);
  • USB OTG;
  • TWAI (двухпроводной автомобильный интерфейс, совместимый с CAN);
  • RMT (дистанционное управление периферийными устройствами для точной генерации и приёма сигналов).

Протокол UART

UART (Universal Asynchronous Receiver/Transmitter) — один из наиболее широко используемых протоколов связи во встроенных системах. Он обеспечивает простой и эффективный способ обмена данными между устройствами с использованием последовательной связи. UART — это, прежде всего, протокол связи «точка-точка», то есть он предназначен для прямого соединения двух устройств: одного в качестве передатчика, а другого в качестве приемника. В отличие от протоколов на основе шины, UART не поддерживает одновременное использование одной и той же линии связи несколькими устройствами без дополнительного оборудования.

Для связи по протоколу UART используются два основных вывода:

  • TX (передатчик) отправляет данные с устройства.
  • RX (Receive) принимает данные от другого устройства.

Протокол является асинхронным, что означает, что он не требует общего тактового сигнала между устройствами. Вместо этого оба устройства должны согласовать общую скорость связи, известную как скорость передачи данных (бод). Данные передаются по одному биту за раз по линии передачи (TX) и принимаются по линии приема (RX), обычно с указанием стартового и стопового битов для обеспечения надлежащей синхронизации.

UART-соединение

Для соединения двух устройств с использованием UART-связи мы соединяем передатчик (TX) первого устройства с приемником (RX) второго устройства. Аналогично, приемник (RX) первого устройства соединяется с передатчиком (TX) второго устройства. Другими словами, TX соединяется с RX, а RX соединяется с TX. Кроме того, мы соединяем заземление (GND) обоих устройств, чтобы они использовали одно и то же опорное напряжение.

Структурная схема UART-соединения

Принцип работы UART

Для работы с протоколом UART необходимо сначала убедиться, что два подключенных устройства могут понимать друг друга. Это достигается путем настройки четырех основных параметров связи:

  • Скорость передачи данных (бод): обозначает скорость связи, измеряемую в битах в секунду (бит/с). Она определяет скорость передачи битов. Оба устройства должны использовать одинаковую скорость передачи данных, чтобы правильно считывать временные параметры сигнала.
  • Длина бит данных: определяет количество бит, используемых для представления фактических данных в каждом кадре. Наиболее распространенное значение — 8 бит, но в зависимости от приложения оно может быть также 5, 6, 7 или 9 бит.
  • Бит четности: используется для базового обнаружения ошибок. Четность может быть следующей:
    • Четная четность: при установке четной четности бит "партия" будет установлен в единицу, если общее количество единиц в данных станет нечетным.
    • Нечетная четность: при установке нечетной четности бит "партии" будет установлен в единицу, если общее количество единиц в данных станет четным.
  • Стоповый бит(ы): указывает на конец кадра данных. Он также дает приемнику время на подготовку к следующей передаче. В распространенных конфигурациях используется 1 или 2 стоповых бита.

После настройки обоих устройств с одинаковыми параметрами UART можно начинать передачу данных.

В режиме ожидания линия передачи UART находится в высоком состоянии (HIGH). Для начала связи передатчик переключает линию TX из высокого состояния в низкое (LOW). Это называется стартовым битом и сигнализирует о начале кадра данных.

После стартового бита передатчик отправляет биты данных по одному, соблюдая заданную скорость передачи данных. Передача битов обычно начинается с младшего значащего бита (LSB).

Далее, если включена проверка четности, передатчик отправляет бит четности для проверки ошибок.

Наконец, передатчик отправляет стоповый бит (биты), устанавливая линию TX обратно в состояние HIGH. Это означает, что передача текущего кадра данных завершена.

Структура кадра UART

Использование протокола UART на ESP32

ESP32-S3 имеет три аппаратных интерфейса UART: UART0, UART1 и UART2. По умолчанию используются следующие наиболее распространенные назначения контактов:

  • Интерфейс UART0 обычно используется для последовательной связи, прошивки и отладки.
    • TX: GPIO 43
    • RX: GPIO 44
  • UART1
    • TX: GPIO 17
    • RX: GPIO 18

Контакты UART в ESP32-S3

Благодаря матрице GPIO контакты UART на ESP32-S3 обладают высокой гибкостью, поэтому при необходимости их можно переназначить на другие совместимые контакты GPIO программным способом.

Программирование интерфейса UART

Теперь, когда мы понимаем принцип работы протокола UART, давайте создадим простой проект, в котором соединим две платы ESP32-S3 с помощью UART-связи. В этом проекте к первой плате ESP32-S3 будет подключена кнопка. При нажатии кнопки плата будет отправлять на вторую плату ESP32-S3 либо «ВКЛ», либо «ВЫКЛ» по протоколу UART. Затем вторая плата ESP32-S3 прочитает полученные данные и включит или выключит светодиод в соответствии с ними.

Начнём со сборки схемы. Для UART-связи между двумя платами ESP32-S3 мы будем использовать контакты GPIO 17 и GPIO 18 в качестве выводов TX и RX. На первой плате ESP32-S3 контакт GPIO 1 будет использоваться для считывания состояния кнопки, а на второй — для управления светодиодом.

Схема соединения двух плат ESP32-S3 через интерфейс UART

После завершения сборки схемы мы можем перейти к написанию программы для первого ESP32-S3. Начнем с импорта библиотек, необходимых для связи по UART. Затем определим константы для порта UART, выводов TX и RX, а также размер буфера.

Далее, внутри функции app_main() мы настраиваем GPIO 1 как входной контакт, подключенный к кнопке. Мы также включаем внутренний подтягивающий резистор, чтобы контакт оставался в низком состоянии, когда кнопка не нажата. Это помогает предотвратить плавающие или шумные сигналы, которые могут привести к некорректным показаниям. После этого мы создаем логическую переменную с именем ledState и инициализируем ее значением false. Эта переменная будет использоваться для хранения текущего состояния, которое позже будет отправлено на второй ESP32-S3.

После настройки вывода GPIO необходимо настроить параметры связи UART. Сначала создадим структуру uart_config_t и зададим параметры UART, такие как скорость передачи данных, биты данных, четность и стоповые биты. В этом проекте мы используем скорость передачи данных 115200 бод, 8 бит данных, отсутствие бита четности и 1 стоповый бит, что является одной из наиболее распространенных конфигураций UART. Мы также отключим аппаратное управление потоком и будем использовать источник тактового сигнала по умолчанию.

Далее мы устанавливаем драйвер UART с помощью функции uart_driver_install(). Эта функция инициализирует драйвер UART и выделяет необходимые ресурсы для связи по UART. Функция принимает пять параметров.

Первый параметр, UART_PORT, указывает, какой периферийный UART мы хотим использовать. Ранее мы определили его как UART_NUM_1, что означает, что для связи будет использоваться UART1 на ESP32-S3.
Второй параметр, BUF_SIZE, определяет размер буфера приема в байтах. В нашем проекте мы установили его равным 1024, что означает, что драйвер UART может хранить до 1024 байт входящих данных до их обработки.
Третий параметр представляет размер буфера передачи. В этом примере мы установили его равным 0, что отключает выделенный буфер TX, поскольку наше приложение простое и не требует буферизованной передачи.
Четвертый параметр определяет размер очереди событий UART. Драйвер UART может генерировать события, такие как прием данных или обнаружение ошибок, и эти события могут храниться в очереди. Поскольку в этом проекте мы не используем события UART, мы устанавливаем это значение равным 0.

Пятый параметр — это указатель на дескриптор очереди событий. Обычно, если мы включили очередь событий UART, этот параметр хранит адрес созданной очереди. Поскольку мы не используем события, мы передаем NULL.
Последний параметр задает флаги выделения прерываний, используемые драйвером UART. Эти флаги можно использовать для настройки способа выделения прерываний в системе ESP32-S3.

После установки драйвера мы применяем параметры конфигурации к выбранному порту UART, используя соответствующую функцию uart_param_config(), передавая порт UART и адрес структуры конфигурации.

Наконец, с помощью соответствующей функции мы назначаем контакты GPIO, которые будут использоваться для связи по UART - uart_set_pin(). В нашем случае GPIO 17 настроен как контакт TX, а GPIO 18 — как контакт RX. Последние два параметра установлены на значение UART_PIN_NO_CHANGE поскольку мы не используем аппаратные контакты управления потоком RTS или CTS.

После завершения настройки UART мы можем начать отправку данных с первого ESP32-S3 на второй. Внутри бесконечного цикла while программа непрерывно считывает состояние GPIO 1, gpio_get_level()чтобы определить, нажата ли кнопка.

При нажатии кнопки мы изменяем значение переменной ledState. Затем, используя функцию, мы отправляем соответствующее сообщение через UART - uart_write_bytes(). Если значение ledState равно true, ESP32-S3 отправляет "ON\r\n"; в противном случае — "OFF\r\n". Последний параметр задает количество отправляемых байтов: 4 байта для "ON\r\n" и 5 байтов для "OFF\r\n".

Наконец, мы с помощью vTaskDelay()приостанавливаем выполнение задачи на 1 секунду, прежде чем повторить цикл.

Мы завершили разработку программы для первого ESP32-S3. Далее мы создадим программу для второго ESP32-S3, который будет принимать данные через UART и управлять светодиодом в зависимости от полученного сообщения.

Как и в первой программе, мы начинаем с подключения необходимых библиотек для FreeRTOS, управления GPIO и связи по UART. Затем мы определяем порт UART, контакты TX и RX, а также размер буфера UART.

Внутри функции app_main() мы настраиваем GPIO 1 как выходной контакт, поскольку он подключен к светодиоду. После этого мы настраиваем параметры UART, используя ту же конфигурацию, что и в первой плате ESP32-S3, чтобы обе платы могли корректно взаимодействовать.

Наконец, мы создаём массив данных размером BUF_SIZE для хранения входящих данных UART. Внутри бесконечного цикла while мы используем функцию uart_read_bytes() для чтения данных, отправленных с первого ESP32-S3.

Эта функция принимает четыре параметра: UART-порт для чтения, массив, в котором будут храниться полученные данные, количество байтов для чтения и значение таймаута, определяющее, как долго ESP32-S3 должен ожидать входящих данных, прежде чем остановить операцию чтения.

После получения данных мы сравниваем полученное сообщение с "ON\r\n" и "OFF\r\n". Если полученные данные равны "ON\r\n", мы устанавливаем GPIO 1 в состояние HIGH, чтобы включить светодиод. В противном случае, если полученные данные равны "OFF\r\n", мы устанавливаем GPIO 1 в состояние LOW, чтобы выключить светодиод.

Протокол I2C

Протокол UART работает безупречно, но имеет небольшое ограничение: это протокол связи «точка-точка». Это означает, что обычно он соединяет только два устройства. Если мы хотим добавить больше устройств, нам потребуется больше контактов и больше независимых последовательных портов. По мере роста числа датчиков и модулей мы быстро используем все доступные контакты. Для решения этой проблемы был разработан новый протокол: I2C.

I2C, что расшифровывается как Inter-Integrated Circuit (межкоммутационная схема), — это протокол последовательной связи, позволяющий нескольким устройствам обмениваться данными, используя всего два провода.

В протоколе I2C используются две линии связи:

  • SDA (Serial Data Line) используется для передачи данных между устройствами.
  • SCL (Serial Clock Line) используется для синхронизации связи с помощью тактового сигнала.

I2C-соединение

Протокол является синхронным, что означает, что связь координируется с помощью общего тактового сигнала. В сети I²C одно устройство выступает в роли ведущего и управляет связью, в то время как одно или несколько ведомых устройств отвечают ведущему. Каждое ведомое устройство имеет уникальный адрес, что позволяет нескольким устройствам работать на одной шине без необходимости использования отдельных линий связи.

  • Главное устройство управляет связью и генерирует тактовый сигнал на линии SCL.
  • Подчиненные устройства реагируют на обращение к ведущему устройству.

Принцип соединения устройств с помощью I2C

При подключении устройств к шине I²C для линий SDA и SCL требуются подтягивающие резисторы, подключенные к напряжению питания (VCC). Это связано с тем, что устройства I²C используют конфигурацию с открытым стоком и открытым коллектором, то есть устройства могут подтягивать линии к низкому уровню, но не могут напрямую подавать на них высокий уровень. Подтягивающие резисторы обеспечивают возврат линий в высокое состояние, когда ни одно устройство активно не подтягивает их к низкому уровню.

Принцип работы I²C

Для работы с протоколом I²C все подключенные устройства должны использовать одну и ту же коммуникационную шину, состоящую из двух линий: SDA для передачи данных и SCL для тактового сигнала. Управление связью осуществляется ведущим устройством, а остальные устройства работают в режиме ведомых.

Перед началом обмена данными каждое ведомое устройство должно иметь уникальный адрес, чтобы ведущее устройство могло идентифицировать его и взаимодействовать с ним индивидуально.

В основе обмена данными по протоколу I²C лежат следующие основные принципы:

  • Связь по схеме «ведущий-ведомый»: ведущее устройство (master) управляет всем процессом связи. Оно генерирует тактовый сигнал на линии SCL и определяет, когда начинается и заканчивается связь.
  • Адресация: каждое ведомое устройство (slave), подключенное к шине, имеет уникальный адрес, обычно 7- или 10-битный. Ведущее устройство отправляет этот адрес для выбора целевого ведомого устройства перед обменом данными.
  • Синхронная связь: передача данных синхронизируется с помощью тактовой линии SCL. В отличие от UART, I²C не требует согласования скорости передачи данных, поскольку ведущее устройство напрямую обеспечивает синхронизацию тактового сигнала.
  • Ответ ACK/NACK: после каждого переданного байта принимающее устройство отправляет подтверждающий бит:
    • ACK (Подтверждение): указывает на то, что данные были успешно получены.
    • NACK (Not Acknowledge): указывает на то, что получатель не принял данные или обмен данными должен быть прекращен.

Когда шина находится в режиме ожидания, линии SDA и SCL остаются в высоком состоянии из-за подтягивающих резисторов.

Для начала связи ведущее устройство генерирует условие START, понижая уровень сигнала на линии SDA до низкого уровня, в то время как линия SCL остается в высоком уровне. Это сигнализирует всем подключенным устройствам о начале передачи.

Далее ведущее устройство отправляет адрес целевого ведомого устройства, за которым следует бит чтения/записи (R/W):

  • 0 → Операция записи (ведущий отправляет данные ведомому)
  • 1 → Операция чтения (ведущее устройство получает данные от ведомого устройства)

После получения адреса выбранное ведомое устройство отвечает битом подтверждения (ACK).

Затем ведущее и ведомое устройства обмениваются данными по одному байту за раз по линии SDA. Каждый байт содержит 8 бит и синхронизируется с тактовыми импульсами на линии SCL. После передачи каждого байта приемник отправляет бит ACK для подтверждения успешного приема.

После завершения обмена данными ведущее устройство генерирует условие STOP, изменяя состояние линии SDA с низкого на высокий, в то время как линия SCL остается в высоком состоянии. Это освобождает шину и возвращает ее в состояние ожидания.

Временные диаграммы работы I2C

Использование протокола UART на ESP32

ESP32-S3 содержит два аппаратных контроллера I²C, отвечающих за управление связью по шине I²C. Контроллеры отвечают за генерацию тактового сигнала, передачу и прием данных, определение условий START и STOP, а также автоматическое управление ответами ACK/NACK.

Каждый контроллер I²C может работать независимо в одном из следующих режимов:

  • Главный узел: управляет связью и генерирует тактовый сигнал.
  • Подчиненное устройство: отвечает на запросы от главного устройства.

Наличие двух независимых контроллеров I²C позволяет ESP32-S3 одновременно взаимодействовать с несколькими шинами I²C, при этом любые контакты GPIO могут быть сконфигурированы как контакты SDA и SCL.

Программирование интерфейса I²C

Теперь, когда мы понимаем принцип работы протокола I²C, давайте восстановим предыдущий пример, используя связь по протоколу I²C вместо UART. В этом проекте две платы ESP32-S3 будут взаимодействовать по шине I²C. Первая плата ESP32-S3 будет выступать в качестве ведущей платы I²C, а вторая — в качестве ведомой платы I²C.

Как и прежде, к первому ESP32-S3 подключена кнопка. При нажатии кнопки ведущая плата отправляет сообщение на второй ESP32-S3 по протоколу I²C. Затем ведомый ESP32-S3 получает сообщение и включает или выключает светодиод в соответствии с ним - "ON"/"OFF"

Для сборки схемы обе платы ESP32-S3 должны использовать общие линии SDA и SCL. В этом примере мы будем использовать:

  • GPIO 8 как SDA
  • GPIO 9 как SCL

Мы также подключаем подтягивающие резисторы между SDA, SCL и 3,3 В. Наконец, мы подключаем вывод GPIO 1 ведущего ESP32-S3 к кнопке, а вывод GPIO 1 ведомого ESP32-S3 — к светодиоду.

Схема соединения двух плат ESP32-S3 через интерфейс I2C

После сборки схемы мы можем приступить к написанию программы для первого ESP32-S3, который будет работать в качестве ведущего устройства по протоколу I²C.

Сначала мы подключаем необходимые библиотеки и определяем константы конфигурации I2C.

Внутри функции app_main() мы сначала настраиваем GPIO 1 как входной контакт, подключенный к кнопке. Мы также включаем внутренний подтягивающий резистор, чтобы избежать плавающих сигналов. Затем мы создаем логическую переменную ledState для хранения текущего состояния светодиода, которое позже будет отправлено на подчиненный модуль ESP32-S3.

Далее мы настраиваем шину I²C Master, используя структуру i2c_master_bus_config_t. В этой структуре мы выбираем контроллер I²C, назначаем контакты SDA и SCL, включаем внутренние подтягивающие резисторы, а также устанавливаем для Master режим игнорирования любых импульсов на линии I2C длительностью менее 7 тактовых циклов.

После создания структуры конфигурации мы устанавливаем главную шину I²C с помощью соответствующей функции i2c_new_master_bus(). Эта функция инициализирует выбранный контроллер I²C и возвращает дескриптор, представляющий шину I²C.

Далее мы настраиваем ведомое устройство, используя структуру i2c_device_config_t. Здесь мы указываем адрес ведомого устройства, задаем длину адреса и устанавливаем частоту тактирования I²C на 100 кГц.

Затем, используя соответствующую функцию, мы добавляем ведомое устройство к ведущей шине I²C - i2c_master_bus_add_device().

После завершения настройки I²C мы можем начать отправку данных на второй ESP32-S3.

Внутри бесконечного цикла while программа постоянно проверяет, нажата ли кнопка, используя функцию gpio_get_level(). При нажатии кнопки мы изменяем значение функции ledState.

Затем мы отправляем либо "ON"либо "OFF"другое ведомому ESP32-S3,  используя функцию i2c_master_transmit(), которая принимает в качестве аргументов дескриптор ведущего устройства I2C, указатель на байты данных, которые будут отправлены по шине I2C, количество байтов для передачи из буфера записи и таймаут передачи в миллисекундах. Мы устанавливаем его равным -1, что означает блокировку и ожидание завершения передачи данных.

Мы завершили разработку программы для первого ESP32-S3. Теперь мы создадим программу для второго ESP32-S3, который будет работать в режиме ведомого устройства по протоколу I²C и управлять светодиодом на основе полученных данных.

Как и прежде, начнем с подключения необходимых библиотек и определения параметров конфигурации I²C.

После этого мы создаём новый тип структуры и используем его для определения контекстной переменной. Эта переменная будет использоваться для чтения и хранения данных, отправляемых с ведущего устройства ESP32-S3 по протоколу I²C.

I2C-ведомое устройство не так активно, как I2C-ведущее, которое знает, когда отправлять данные, а когда их принимать. В большинстве случаев I2C-ведомое устройство очень пассивно, то есть его способность отправлять и принимать данные в значительной степени зависит от действий ведущего устройства. Поэтому мы реализовали в драйвере функцию обратного вызова для обработки запросов на запись от I2C-ведущего устройства.

Функция обратного вызова должна иметь три аргумента: дескриптор для ведомого устройства I2C, данные события приема I2C, данные, передаваемые драйвером ведущему устройству, и, наконец, параметр, представляющий дополнительные данные.

В нашем случае для функции обратного вызова мы просто считываем данные, отправленные главным Esp32s3, и редактируем файл context.data, чтобы он хранил сообщение.

Внутри функции app_main() мы сначала настраиваем GPIO 1 как выходной контакт, поскольку он подключен к светодиоду.

Далее мы настраиваем ведомое устройство I²C, используя структуру i2c_slave_config_t. Здесь мы выбираем контроллер I²C, назначаем контакты SDA и SCL, настраиваем адрес ведомого устройства и определяем размеры буферов программного обеспечения.

После настройки параметров ведомого устройства мы инициализируем ведомое устройство I²C с помощью соответствующей функции i2c_new_slave_device().

Мы создаём структуру конфигурации обратного вызова событий и регистрируем её в драйвере ведомого устройства I²C. Это позволяет ESP32-S3 выполнять функцию обратного вызова всякий раз, когда данные поступают от ведущего устройства.

Наконец, мы создаём буфер для хранения полученных данных. Внутри бесконечного цикла while ведомое устройство непрерывно проверяет полученные данные и соответствующим образом обновляет состояние светодиода.

Протокол SPI

Протокол I²C решает проблему подключения нескольких устройств с использованием всего двух проводов, но имеет небольшое ограничение: он использует одну линию передачи данных как для отправки, так и для приема, работает в полудуплексном режиме, не может одновременно отправлять и получать данные, а совместное использование шины многими устройствами требует адресации, что увеличивает накладные расходы. Для достижения более быстрой и простой связи обычно используется другой протокол: SPI .

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

Вместо двух проводов в протоколе SPI обычно используются четыре линии связи:

  • MOSI (Master Out Slave In) используется для передачи данных от ведущего устройства к ведомому.
  • MISO (Master In Slave Out) используется для передачи данных от ведомого устройства к ведущему.
  • SCLK (Serial Clock) используется для синхронизации связи с помощью тактового сигнала.
  • CS (Chip Select) / SS (Slave Select) используются ведущим устройством для выбора конкретного ведомого устройства.

SPI-соединение

Протокол является синхронным, что означает, что связь координируется с помощью общего тактового сигнала. В сети SPI одно устройство выступает в роли ведущего и управляет связью, в то время как одно или несколько ведомых устройств отвечают ведущему. В отличие от I2C, SPI не использует адреса. Вместо этого ведущее устройство использует выделенную линию выбора микросхемы (CS) для каждого ведомого устройства, чтобы выбрать, с каким устройством взаимодействовать.

  • Главное устройство управляет связью, генерирует тактовый сигнал на линии SCLK и управляет линиями CS.
  • Подчиненные устройства прослушивают линию MOSI и передают данные обратно по линии MISO, когда активируется их конкретная линия CS.

При подключении устройств по шине SPI линии MOSI, MISO и SCLK используются совместно всеми устройствами. Однако каждому ведомому устройству требуется отдельная линия CS, подключенная к ведущему устройству. В SPI используются двухтактные драйверы, то есть линии активно управляются как высоким, так и низким уровнем, что позволяет достигать гораздо более высоких скоростей без необходимости использования внешних подтягивающих резисторов, требуемых для I2C.

Принцип соединения устройств с помощью SPI

Принцип работы SPI

Для работы с протоколом SPI все подключенные устройства должны использовать общие линии шины (MOSI, MISO, SCLK), а ведущее устройство должно иметь отдельную линию CS для каждого ведомого устройства. Инициирование и управление связью всегда осуществляется ведущим устройством.

Когда шина находится в режиме ожидания, линия SCLK находится в состоянии покоя (высокий или низкий уровень в зависимости от настроенного режима), а линии CS поддерживаются в высоком состоянии, чтобы неактивировать ведомые устройства.

Для начала связи ведущее устройство переводит линию CS ведомого устройства в низкий уровень. Это сигнализирует конкретному устройству о начале передачи.

Далее ведущее устройство начинает генерировать тактовые импульсы на линии SCLK. За каждый такт ведущий устройство передает один бит данных по линии MOSI, а ведомое устройство — один бит по линии MISO. Благодаря архитектуре сдвигового регистра обмен данными происходит постоянно.

После завершения обмена данными ведущее устройство отключает тактовый сигнал и возвращает линию CS в высокое состояние. Это освобождает ведомое устройство и возвращает шину в состояние ожидания.

Временные диаграммы работы SPI

Использование протокола SPI d ESP32

ESP32-S3 включает в себя четыре SPI-контроллера.

  • SPI0 и SPI1 в основном предназначены для внутренней связи с внешней флэш-памятью и PSRAM.
  • SPI2 и SPI3 доступны для приложений общего назначения.

Каждый SPI-контроллер может работать независимо в одном из двух режимов:

  • Режим Master (ведущее устройство): ESP32-S3 управляет связью, генерирует тактовый сигнал последовательного порта (SCLK) и управляет сигналом выбора микросхемы (CS).
  • Режим ведомого устройства: ESP32-S3 реагирует на транзакции, инициированные внешним SPI-ведущим устройством.

Через матрицу GPIO ESP32-S3 контакты SPI2 и SPI3 могут быть подключены практически к любым доступным выводам GPIO, что обеспечивает гибкое назначение контактов для сигналов SPI, таких как MOSI, MISO, SCLK и CS.

Программирование интерфейса SPI

Теперь, когда мы понимаем принцип работы протокола SPI, давайте реализуем предыдущий пример, используя связь по SPI. В этом проекте две платы ESP32-S3 будут взаимодействовать по шине SPI. Первая плата ESP32-S3 будет выступать в роли ведущего устройства SPI, а вторая — в роли ведомого устройства SPI.

Как и прежде, к первому ESP32-S3 подключена кнопка. При нажатии кнопки ведущая плата отправляет сообщение на второй ESP32-S3 по протоколу SPI. Затем ведомый ESP32-S3 получает сообщение и включает или выключает светодиод в соответствии с ним - "ON"/"OFF"

Для сборки схемы обе платы ESP32-S3 должны использовать общие линии MOSI, MISO, SCLK и CS. В этом примере мы будем использовать:

  • GPIO 11 в режиме MOSI
  • GPIO 12 как MISO
  • GPIO 13 как SCLK
  • GPIO 10 в качестве CS

Наконец, мы подключаем вывод GPIO 1 ведущего ESP32-S3 к кнопке, а вывод GPIO 1 ведомого ESP32-S3 — к светодиоду.

Схема соединения двух плат ESP32-S3 через интерфейс SPI

После сборки схемы мы можем приступить к написанию программы для первого ESP32-S3, который будет работать в качестве ведущего устройства по интерфейсу SPI.

Сначала мы подключаем необходимые библиотеки и определяем константы конфигурации SPI.

Внутри функции app_main() мы сначала настраиваем GPIO 1 как входной контакт, подключенный к кнопке. Мы также включаем внутренний подтягивающий резистор, чтобы избежать плавающих сигналов. Затем мы создаем логическую переменную ledState для хранения текущего состояния светодиода, которое позже будет отправлено на подчиненный модуль ESP32-S3.

Далее мы настраиваем шину SPI, используя структуру spi_bus_config_t. В этой структуре мы назначаем контакты MOSI, MISO и SCLK, а также определяем максимальный размер передаваемых данных.

После создания конфигурации шины мы устанавливаем ведущую шину SPI, используя соответствующую функцию spi_bus_initialize().

Далее мы настраиваем параметры ведомого устройства, используя структуру spi_device_interface_config_t. Здесь мы указываем тактовую частоту (1 МГц) clock_speed_hz, режим SPI mode и назначаем вывод CS spics_io_num. Наконец, мы устанавливаем размер очереди транзакций. Это определяет, сколько транзакций может одновременно находиться «в сети».

Затем мы добавляем ведомое устройство к шине SPI с помощью команды spi_bus_add_device().

После завершения настройки SPI мы можем начать отправку данных на второй ESP32-S3.

Внутри бесконечного цикла while программа постоянно проверяет, нажата ли кнопка. При нажатии кнопки мы изменяем значение параметра ledState.

Для отправки данных мы используем структуру spi_transaction_t. Мы определяем длину нашей транзакции в битах и ​​​​присваиваем данные буферу передачи, а затем отправляем их с помощью spi_device_transmit().

Мы завершили разработку программы для первого ESP32-S3. Теперь мы создадим программу для второго ESP32-S3, который будет работать в режиме SPI-ведомого и управлять светодиодом на основе полученных данных.

Как и прежде, начнем с подключения необходимых библиотек и определения параметров конфигурации SPI.

Внутри функции app_main() мы сначала настраиваем GPIO 1 как выходной контакт, поскольку он подключен к светодиоду.

Далее мы настраиваем шину SPI и интерфейс ведомого устройства SPI. Мы используем ту же структуру spi_bus_config_t для назначения контактов, а затем используем spi_slave_interface_config_t для настройки режима ведомого устройства и контакта CS.

После настройки параметров мы инициализируем ведомое устройство SPI с помощью соответствующей функции spi_slave_initialize().

Наконец, мы создаём буфер для хранения данных, которые будем получать от ведущего устройства SPI. Мы также создаём структуру транзакции, задаём длину данных и указываем указатель на буфер, где будут храниться полученные данные. Внутри бесконечного цикла мы подготавливаем транзакцию и передаём её драйверу SPI с помощью метода spi_slave_transmit().

Функция spi_slave_transmit() блокируется и пассивно ожидает, пока ведущий SPI-модуль не отправит данные. После получения данных мы проверяем полученную команду и включаем или выключаем светодиод в зависимости от того, отправил ли ведущий ESP32-S3 команду "ON" или  "OFF".

(Проголосуй первым!)
Загрузка...
1 просмотров

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *