ESP32-C3: драйвер SPI Master Печать
Добавил(а) microsin   

Драйвер SPI Master это код библиотек ESP-IDF для управления периферийными устройствами SPI, которые функционируют как главное устройство на шине.

На кристалле ESP32-C3 интегрировано 2 периферийных устройства SPI.

SPI0/1 используются внутренне для доступа к подключенной памяти flash (эта память может быть как внутренняя, так внешняя). Оба этих контроллера используют общие выводы корпуса чипа для сигналов шины SPI (VDD_SPI, FLASH_HOLD#, FLASH_WP#, FLASH_CS#, FLASH_SCK, FLASH_SDI, FLASH_SDO, подробнее см. [2, 3]). Существует специальный арбитр, который определяет, какой из блоков SPI0 и SPI1 работает с этими внешними выводами в любой момент времени. В настоящее время драйвер SPI Master не поддерживает шину SPI1.
SPI2 это контроллер SPI общего назначения. У него отдельная независимая шина, на которой есть возможность генерировать 6 сигналов выборки (CS), чтобы управлять 6 подчиненными устройствами (SPI slave).

Расшифровка общих терминов, относящихся к драйверу SPI master, приведены в следующей таблице.

Термин Определение
Host, хост Периферийное устройство контроллера SPI, находящееся на кристалле ESP32-C3, которое инициирует и управляет SPI-транзакциями на шине, работая как главное устройство (SPI Master).
Device, устройство Подчиненное устройство SPI. Шина SPI может быть соединена с одним или большим количеством устройств. Каждое устройство совместно использует общие сигналы MOSI, MISO и SCLK, однако устройство активируется только тогда, когда на его отдельную выборку (сигнал Chip Select, CS) хост выдаст разрешающий сигнал.
Bus Сигнальная шина, общая для всех устройств, подключенных к хосту. В общем случае шина SPI состоит из сигналов MISO, MOSI, SCLK, одного или большего количества сигналов CS, а также (опционально) сигналов QUADWP и QUADHD. Таким образом, устройства могут быть подключены к одним и тем же сигналам параллельно, за исключением индивидуальных сигналов выборки CS. Несколько устройств также могут использовать один и тот же сигнал CS, если по данным они соединены друг с другом последовательно, в цепочку.
MOSI Master Out Slave In, сигнал данных от хоста к устройству. Также сигнал data0 в режиме Octal/OPI.
MISO Master In Slave Out, сигнал данных от устройства к хосту. Также сигнал data1 в режиме Octal/OPI.
SCLK Serial Clock, такты данных. Тактовая частота, генерируемая хостом, которая используется для синхронизации передаваемых и принимаемых бит данных.
CS Chip Select, выборка устройства. Позволяет хосту выбрать на шине отдельное устройство (устройства), чтобы можно было обмениваться с ним данными.
QUADWP Write Protect, сигнал защиты от записи. Используется для 4-разрядных (qio/qout) транзакций. Также используется для сигнала data2 в режиме Octal/OPI.
QUADHD Hold, сигнал удержания. Используется для 4-разрядных (qio/qout) транзакций. Также используется для сигнала data3 в режиме Octal/OPI.
DATA4 Data4, сигнал в режиме Octal/OPI.
DATA5 Data5, сигнал в режиме Octal/OPI.
DATA6 Data6, сигнал в режиме Octal/OPI.
DATA7 Data7, сигнал в режиме Octal/OPI.
Assertion Действие по активации сигналов данных.
De-assertion Действие по возврату шины в неактивное (idle) состояние.
Transaction Транзакция, состояние с активацией одной из выборок CS, когда осуществляется обмен блоком данных с одним из устройтств на шине (чтение и/или запись), после завершения которого сигнал выборки CS снимается. Транзакции выполняются атомарно, т. е. выполняющаяся транзакция не может быть прервана другой транзакцией.
Launch edge Перепад сигнала тактов, на котором регистр источника данных выводит бит (биты) на шину.
Latch edge Перепад сигнала тактов, на котором регистр получения данных защелкивает значение бита (бит) на шине.

[Возможности драйвера SPI master]

Драйвер SPI master обслуживает коммуникации между хостами и устройствами. Поддерживаются следующие функциональные возможности:

· Многопоточность.
· Прозрачные транзакции DMA для чтения и записи данных.
· Автоматическое мультиплексирование данных с разделением по времени. Данные могут поступать с различных устройств по одной и той же шине сигналов, см. врезку "SPI Bus Lock".

Предупреждение: у драйвера SPI master существует концепция подключения нескольких устройств к одной шине (совместное использование одного периферийного устройства ESP32-C3 SPI). Пока к каждому устройству осуществляется доступ только из одной задачи, драйвер считается безопасным для многопоточного окружения (thread safe). Однако если несколько задач пытаются обращаться к одному и тому же устройству SPI, то драйвер становится не потокобезопасным (not thread-safe). В этом случае рекомендуются варианты решения проблемы:

· Переписать приложение таким образом, чтобы одновременно к каждому периферийному устройству SPI обращалась только одна задача. Вы можете использовать spi_bus_config_t::isr_cpu_id, чтобы зарегистрировать SPI ISR для одного и того же ядра CPU как задачи, относящиеся к периферийному устройству SPI, чтобы обеспечить безопасность потоков.
· Добавить блокировку на мьютексе вокруг общего устройства, используя xSemaphoreCreateMutex.

Для реализации переключения различных устройств, подключенных к разным драйверам, включая SPI Master, SPI Flash, и т. п., применяется блокировка на каждой шине SPI (SPI bus lock). Драйверы могут подключать свои устройства к шине с помощью арбитража путем блокировки.

Каждая блокировка шины инициализируется зарегистрированной службой фоновых операций (background service, BG). Все устройства, которые запрашивают транзакции на шине, должны ждать, пока BG не будет успешно запрещена.

BG является кэшем для шины SPI1. Блокировка шины запретит кэш перед началом операции с устройством, и разрешит её снова после отого, как устройство осободит блокировку. Ни одному из устройств на SPI1 не разрешается использовать ISR, поскольку бессмысленно для задачи уступать контекст для других задач, когда кэш запрещен.

Драйвер SPI Master не поддерживает шину SPI1. Только драйвер SPI Flash может подключиться к этой шине.

Для других шин драйвер может зарегистрировать ISR в качестве BG. Если задача (task) устройства запрашивает исключительный доступ к шине, то bus lock заблокирует выполнение задачи, запретит ISR, и разблокирует выполнение задачи. После того, как задача освободит блокировку, блокировка будет пытаться заново разрешить ISR, если в ISR все еще существуют ожидающие транзакции.

[Транзакции SPI]

Транзакция на шине SPI состоит из 5 фаз, показанных в следующей таблице. Любая из этих фаз может быть пропущена.

Фаза Описание
Command На этой фазе хостом записывается на шину команда (0-16 бит).
Address На этой фазе хост передает на шину адрес (0-32 бита).
Dummy Эта фаза конфигурируется, и используется для удовлетворения требований к таймингу.
Write Хост посылает данные на устройство. Эти данные следуют за опциональными фазами, и на электрическом уровне они неотличимы от этих фаз.
Read Устройство посылает данные хосту.

Атрибуты транзакции определяются структурой конфигурации шины spi_bus_config_t, структурой конфигурации устройства spi_device_interface_config_t, и структурй конфигурации транзакции spi_transaction_t.

Хост SPI может посылать полнодуплексные транзакции, во время которых фазы чтения и записи происходят одновременно. Общая длина транзакции определяется следующими полями структур:

spi_device_interface_config_t::command_bits
spi_device_interface_config_t::address_bits
spi_transaction_t::length

В то же время поле spi_transaction_t::rxlength определяет только длину данных, принятых в буфер.

В полнодуплексных транзакциях фазы чтения и записи не одновременные (происходят в одном направлении). Длины фаз чтения и записи определяются соответственно spi_transaction_t::length и spi_transaction_t::rxlength.

Фазы command и address опциональны, поскольку не каждое устройство SPI требует предварительной команды и/или адреса. Как отражается в конфигурации устройства: если spi_device_interface_config_t::command_bits и/или spi_device_interface_config_t::address_bits установлены в 0, то не будет фазы command и/или address.

Фазы чтения (read) и записи (write) также могут быть опциональными, поскольку не каждая транзакция может требовать одновременных записи и чтения данных. Если поле spi_transaction_t::rx_buffer установлено в NULL, и SPI_TRANS_USE_RXDATA не установлен, то фаза чтения пропускается. Если поле spi_transaction_t::tx_buffer установлено в NULL, и SPI_TRANS_USE_TXDATA не установлен, то фаза записи пропускается.

Драйвер поддеживает 2 типа транзакций interrupt и polling. Программист может выбрать для каждого устройства свой тип транзакции. Если ваше устройство требует транзакций обоих типов, см. врезку "Замечания по отправке смешанных транзакций на одно и то же устройство SPI".

Interrupt-транзакции. Транзакции с прерываниями заблокируют подпрограмму, которая вызвала эту транзакцию, пока транзакция не завершится. Таким образом, этот тип транзакций позволяет CPU выполнять другие задачи.

Задача приложения может поставить в очередь несколько транзакций, и драйвер будет автоматически их обрабатывать по одной в обработчике прерывания (interrupt service routine, ISR). Это позволит задаче переключиться на другие процедуры, пока не завершаться все транзакции.

Polling-транзакции. Транзакции с опросом статуса (polling) не используют прерывания. Подпрограмма, вызывавшая транзакцию, опрашивает биты состояния хоста (SPI Host status), пока транзакция не завершится.

Все задачи, которые используют interrupt-транзакции могут блокироваться на очереди. В этой точке они должны будут ждать, пока ISR не запустится дважды перед завершением транзакции. Polling-транзакции экономят время, затрачиваемое на обработку очереди и переключение контекста, что уменьшает время транзакции. Недостаток polling-транзакции в том, что CPU занят, пока выполняются транзакции.

Подпрограмма spi_device_polling_end() вносит накладные расходы как минимум 1 мкс для разблокировки других задач, когда транзакция завершается. Настоятельно рекомендуется обернуть последовательность polling-транзакций функциями spi_device_acquire_bus() и spi_device_release_bus(), чтобы избежать этих накладных расходов. Для дополнительной информации см. "Bus Acquiring".

Transaction Line Mode. Поддерживаемые линейные режимы шины для ESP32-C3 перечислены в следующей таблице. Для упрощения использования этих режимов установите флаги в поле flags структуры spi_transaction_t, как показано в столбце "Transaction Flag". Ширина шины данных указана в столбцах Command, Address и Data. Если вы хотите проверить, установлены ли соответствующие выводы IO или нет, см. поле flags структуры spi_bus_config_t как показано в столбце "Флаг настройки Bus IO".

Имя режима Command Address Data Флаг транзакции Флаг настройки Bus IO
Normal SPI 1 1 1 0 0
Dual Output 1 1 2 SPI_TRANS_MODE_DIO SPICOMMON_BUSFLAG_DUAL
Dual I/O 1 2 2 SPI_TRANS_MODE_DIOSPI_TRANS_MULTILINE_ADDR  
Quad Output 1 1 4 SPI_TRANS_MODE_QIO SPICOMMON_BUSFLAG_QUAD
Quad I/O 1 4 4 SPI_TRANS_MODE_QIO | SPI_TRANS_MULTILINE_ADDR

#define SPI_TRANS_MODE_DIO            (1<<0)  ///< Передача/прием данных в 2-битном режиме
#define SPI_TRANS_MODE_QIO            (1<<1)  ///< Передача/прием данных в 4-битном режиме
#define SPI_TRANS_USE_RXDATA          (1<<2)  ///< Прием данных в буфер rx_data (поле структуры
                                              ///  spi_transaction_t) вместо буфера rx_buffer.
#define SPI_TRANS_USE_TXDATA          (1<<3)  ///< Передача из буфера tx_data (поле структуры
                                              ///  spi_transaction_t) вместо tx_buffer. Когда
                                              ///  используется этот флаг, не устанавливайте
                                              //   значение указателя tx_buffer.
#define SPI_TRANS_MODE_DIOQIO_ADDR    (1<<4)  ///< Также адрес передачи в режиме, выбранном
                                              ///  SPI_MODE_DIO/SPI_MODE_QIO
#define SPI_TRANS_VARIABLE_CMD        (1<<5)  ///< Использовать command_bits в структуре
                                              ///  spi_transaction_ext_t вместо значения
                                              ///  по умолчанию в spi_device_interface_config_t.
#define SPI_TRANS_VARIABLE_ADDR       (1<<6)  ///< Использовать address_bits в структуре
                                              ///  spi_transaction_ext_t вместо значения
                                              ///  по умолчанию в spi_device_interface_config_t.
#define SPI_TRANS_VARIABLE_DUMMY      (1<<7)  ///< Использовать dummy_bits в структуре
                                              ///  spi_transaction_ext_t вместо значения
                                              ///  по умолчанию в spi_device_interface_config_t.
#define SPI_TRANS_CS_KEEP_ACTIVE      (1<<8)  ///< Удерживать CS активным после передачи данных.
#define SPI_TRANS_MULTILINE_CMD       (1<<9)  ///< Сигналы данных, используемые на фазе команды
                                              ///  те же самые, что и на фазе данных (иначе
                                              ///  только сигналы данных используются на
                                              ///  фазе команды).
#define SPI_TRANS_MODE_OCT            (1<<10) ///< Передача/прием данных в 8-битном режиме.
#define SPI_TRANS_MULTILINE_ADDR SPI_TRANS_MODE_DIOQIO_ADDR ///< Сигналы данных, используемые
                                              ///  на фазе адреса, те же самые, что и на фазе
                                              ///  данных (иначе только сигналы данных
                                              ///  используются на фазе адреса).

Поле uint32_t flags описывает возможности шины, проверяемые драйвером. В это поле записывается объединенные по OR значения флагов SPICOMMON_BUSFLAG_*.

SPICOMMON_BUSFLAG_SLAVE Инициализирует I/O в подчиненном режиме (slave mode).

SPICOMMON_BUSFLAG_MASTER Инициализирует I/O в главном режиме (master mode).

SPICOMMON_BUSFLAG_IOMUX_PINS Проверка использования iomux выводов. Или показывает, что выводы сконфигурированы через мультиплексор ввода/вывода (IO mux) вместо матрицы портов (GPIO matrix).

SPICOMMON_BUSFLAG_GPIO_PINS Принуждает маршрутизировать сигналы через матрицу портов (GPIO matrix). Или показывает, что выводы маршрутизируются через GPIO matrix.

SPICOMMON_BUSFLAG_SCLK Проверяет существование вывода SCLK. Или показывает, инициализирован ли сигнал тактов SPI.

SPICOMMON_BUSFLAG_MISO Проверяет существование вывода MISO. Или показывает, инициализирован ли сигнал MISO.

SPICOMMON_BUSFLAG_MOSI Проверяет существование вывода MOSI. Или показывает, инициализирован ли сигнал MOSI.

SPICOMMON_BUSFLAG_DUAL Проверяет, могут ли выводы MOSI и MISO работать на вывод. Или показывает, может ли шина работать в режиме DIO.

SPICOMMON_BUSFLAG_WPHD Проверяет существование выводов WP и HD. Или показывает, что выводы WP и HD инициализированы.

SPICOMMON_BUSFLAG_QUAD Проверяет существование выводов MOSI/MISO/WP/HD в качестве выходов. Или показывает, может ли шина работать в режиме QIO.

SPICOMMON_BUSFLAG_IO4_IO7 Проверяет существование выводов IO4~IO7. Или показывает, что выводы IO4~IO7 инициализированы.

SPICOMMON_BUSFLAG_OCTAL Проверяет существование выводов MOSI, MISO, WP, HD, SPIIO4, SPIIO5, SPIIO6, SPIIO7 в качестве выходов. Или показывает, может ли шина работать в 8-битном режиме (octal mode).

SPICOMMON_BUSFLAG_NATIVE_PINS Проверяет существование выводов в традиционном режиме.

Фазы Command и Address. Во время фаз команды и адреса поля spi_transaction_t::cmd и spi_transaction_t::addr посылаются на шину, в это время ничего с шины не считывается. Длины по умолчанию для фаз command и address устанавливаются в spi_device_interface_config_t путем вызова spi_bus_add_device(). Если флаги SPI_TRANS_VARIABLE_CMD и SPI_TRANS_VARIABLE_ADDR в поле spi_transaction_t::flags не установлены, то драйвер автоматически установит длины этих фаз в значения по умолчанию во время инициализации устройства.

Если длины фаз command и address должны быть переменными, то декларируйте структуру spi_transaction_ext_t, установите флаги SPI_TRANS_VARIABLE_CMD и/или SPI_TRANS_VARIABLE_ADDR в поле spi_transaction_ext_t::base и сконфигурируйте остальную часть base как обычно. Затем длина каждой фазы будет равна полям spi_transaction_ext_t::command_bits и spi_transaction_ext_t::address_bits в структуре spi_transaction_ext_t.

Если фазы command и address phase должны использовать одно и то же количество сигналов, что и фаза данных, то нужно установить SPI_TRANS_MULTILINE_CMD и/или SPI_TRANS_MULTILINE_ADDR в поле flags структуры spi_transaction_t. См. также выше секцию "Transaction Line Mode".

Фазы Write и Read. Обычно данные, которые нужно послать в устройство или получить из устройства будут записаны или прочитаны из блока памяти, определяемого полями spi_transaction_t::tx_buffer и spi_transaction_t::rx_buffer. Если DMA разрешен для передачи, то буферы должны быть:

1. Выделены во внутренней памяти (обычно это IRAM), которая допускает обращения для прямого достуа (DMA-capable internal memory). Если разрешена внешняя PSRAM, то это означает использование pvPortMallocCaps(size, MALLOC_CAP_DMA).

2. Выровненные на байтовый адрес, нацело делящийся на 4 (32-bit aligned), и длина буфера в байтах также должна делиться нацело на 4.

Если эти требования не удовлетворяются, то то эффективность транзакций снизится, потому что будут вовлекаться дополнительные операции по выделению временных буферов и копированию в них данных.

Если используются больше одной линии данных для передачи, то установите флаг SPI_DEVICE_HALFDUPLEX в поле flags структуры spi_device_interface_config_t. И поле flags в структуре spi_transaction_t должно быть установлено, как это описано выше в секции "Transaction Line Mode".

Примечание: не поддерживаются полудуплексные транзакции, на которых одновременно действуют фазы read и write. Для этого используйте полнодуплексный режим.

Bus Acquiring. Иногда вам может потребоваться посылать транзакции SPI эксклюзивно и непрерывно, чтобы они занимали минимально возможное время. Для этого можно использовать захват шины (bus acquiring), что помогает приостановить транзакции (обоих типов, как polling, так и interrupt) для других устройств, пока шина не будет освобождена. Чтобы захватить (acquire) и освободить (release) шину, используйте функции spi_device_acquire_bus() и spi_device_release_bus().

[Использование драйвера SPI master]

1. Инициализируйте шину SPI вызовом spi_bus_initialize(). Убедитесь в правильной установке выводов I/O в структуре spi_bus_config_t. Сигналы, которые не нужны, установите в -1.

2. Зарегистрируйте устройство, подключенное к шине, для драйвера путем вызова функции spi_bus_add_device(). Убедитесь, что сконфигурировали любые тайминги, требуемые для устройства, с помощью параметра dev_config. Теперь вы должны получить дескриптор (handle) устройства, который будет использоваться для транзакций с ним.

3. Чтобы взаимодействовать с устройством через драйвер, заполните одну или большее количество структур spi_transaction_t любыми требыемыми для транзакции параметрами. Затем отправьте структуры либо с использованием polling-транзакции, либо interrupt-транзакции:

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

Polling. Вызовите функцию spi_device_polling_transmit() для отправки транзакций с опросом (polling). Альтернативно, если вы хотите что-нибудь вставить между ними, используйте spi_device_polling_start() и spi_device_polling_end().

4. (Не обязательно) Для выполнения обратных (back-to-back) транзакций с устройством вызовите функцию spi_device_acquire_bus() перед отправкой транзакций, и spi_device_release_bus() после отправки транзакций.

5. (Не обязательно) Для выгрузки драйвера для определенного устройства вызовите spi_bus_remove_device(), передав в неё в качестве аргумента дескриптор (handle) устройства.

6. (Не обязательно) Чтобы удалить драйвер для шины, проверьте, что нет больше подключенных драйверов, и вызовите spi_bus_free().

Код примера для драйвера SPI master можно найти в директории peripherals/spi_master среди примеров кода ESP-IDF.

Транзакции данных, не превышающих 32 бита. Когда размер транзакций данных в битах равен 32 или меньше, выделение специального буфера для них не будет оптимальным решением. Вместо этого данные можно напрямую сохранить в структуру транзакции. Для передаваемых данных этого можно достичт использованием поля spi_transaction_t::tx_data, и установкой на передаче флага SPI_TRANS_USE_TXDATA. Для принимаемых данных используйте spi_transaction_t::rx_data, и установите SPI_TRANS_USE_RXDATA. В обоих случаях не трокайте поля spi_transaction_t::tx_buffer или spi_transaction_t::rx_buffer, потому что будут использоваться те же самые ячейки памяти spi_transaction_t::tx_data и spi_transaction_t::rx_data.

Транзакции с целочисленными данными, отличающимися от uint8_t. Хост SPI читает данные из памяти и записывает данные в память байт за байтом. По умолчанию данные посылаются старшим битом (most significant bit, MSB) вперед, поскольку обратный порядок (когда младший бит LSB идет первым) используется в редких случаях. Если должно быть отправлено значение, меньшее чем 8 бит, то биты должны быть записаны в память так, чтобы бит MSB (старший бит) был первым.

Например, если надо послать 0b00010, то это значение нужно записать в переменную uint8_t, и длину для чтения надо установить на 5 бит. Устройство все еще примет 8 бит с 3 дополнительными "случайными" битами, так что чтение должно быть выполнено корректно.

В дополнение к сказанному ESP32-C3 это чип с архитектурой little-endian [4]. Это означает, что самый младший значащий байт переменных uint16_t и uint32_t сохраняется по самому малому адремсу. Таким образом, если в памяти сохранено uint16_t, то сначала будут отправлены биты [7:0] этого числа, а за ними биты [15:8].

Для случаев, когда данные для отправки должны имеит размер, отличающийся от массивов uint8_t, можно использовать следующие макросы для преобразования данных в формат, который можно напрямую послать драйверу SPI: SPI_SWAP_DATA_TX для передаваемых данных, SPI_SWAP_DATA_RX для принимаемых данных.

Чтобы снизить сложность кода, посылайте на устройство транзакции только одного типа (interrupt или polling). Однако все еще есть возможность посылать попеременно interrupt-транзакции и polling-транзакции. Здесь объясняется, как это можно сделать.

Polling-транзакции должны инициироваться только после того, как завершены все polling-транзакции и interrupt-транзакции.

Поскольку не завершенная polling-транзакция блокирует другие транзакции, не забудьте вызвать spi_device_polling_end() после spi_device_polling_start(), чтобы позволить использовать шину другим транзакциями или устройствам. Помните, что если здесь не нужно переключаться на другие задачи во время вашей polling-транзакции, то вы можете инициировать транзакцию через spi_device_polling_transmit(), чтобы она была завершена автоматически.

Выполняющиеся polling-транзакции нарушаются работой ISR для размещения interrupt-транзакций. Перед тем как вызвать spi_device_polling_start() всегда убедитесь, что все interrupt-транзакции, отправленные в ISR, были завершены. Для этого вызывайте spi_device_get_trans_result() пока не будут возвращены все транзакции.

Чтобы лучше контролировать последовательность вызова функций, посылайте смешанные транзакции в одно и то же устройство только в пределах одной задачи (task).

GPIO Matrix и IO_MUX. Большинство сигналов периферийного устройства чипа напрямую соединены с их выделенными выводами IO_MUX. Однако сигналы также могут быть направлены на любой доступный вывод с помощью обходного пути, с помощью GPIO matrix. Если хотя бы один сигнал периферийного устройства направлен через GPIO matrix, то все его сигналы будут направлены через GPIO matrix.

Когда хост SPI установлен на частоту 80 МГц или ниже, то роутинг выводов SPI через GPIO matrix будет вести себя так же, как и с роутингом через IO_MUX.

В следущей таблице показан роутинг выводов IO_MUX для шины SPI.

Имя вывода Номер GPIO (SPI2)
CS0(*) 10
SCLK 6
MISO 2
MOSI 7
QUADWP 5
QUADHD 4

Примечание (*): только первое устройство, подключенное к шине, может использовать вывод CS0.

[Замечания по скорости передачи]

Существуют 3 фактора, ограничивающие скорость передачи:

· Интервал транзакции.
· Тактовая частота SPI.
· Промахи кэша функций SPI, включая callback-функции.

Основной параметр, ограничивающий скорость передачи для больших транзакций - тактовая частота. Для нескольких малентких транзакций скорость передачи определяется главным образом интервалами между транзакциями.

Длительность транзакции. Длительность транзакции включает настройку регистров периферийного устройства SPI, коирование данных в FIFO или настройку связей DMA, и время транзакции SPI.

Interrupt-транзакции позволяют добавить некоторые дополнительную накладные расходы, чтобы приспособить учесть стоимость очередей FreeRTOS и время, необходимое для переключения между задачами и ISR.

Для interrupt-транзакций CPU может переключаться на другие задачи, когда транзакция находится в процессе выпоолнения. Это экономит время CPU, но увеличивает время транзакции (см. выше описание в секции "Interrupt-транзакции"). Для polling-транзакций задача не блокируется, однако имеется возможность опроса статуса транзакции, чтобы определить момент её завершения (см. выше описание в секции "Polling-транзакции").

Если DMA разрешен, то настройка связанного списка транзакции требует около 2 мкс на транзакцию. Когда master передает данные, он автоматически считывает данные из связанного списка. Если DMA не разрешен, то CPU сам должен записать и прочитать каждый байт FIFO. Обычно это происходит быстрее, чем 2 мкс, однако длина транзакции ограничена 64 байтами как для записи, так и для чтения.

Типовые длительности транзакции для одного байта данных показаны ниже.

Interrupt-транзакция с DMA: 28 мкс.
Interrupt-транзакция с CPU: 27 мкс.
Polling-транзакция с DMA: 10 мкс.
Polling-транзакция с CPU: 9 мкс.

Тактовая частота SPI. Источник тактов периферийных устройств GPSPI может быть выбран установкой spi_device_handle_t::cfg::clock_source. Поддерживаемые источники тактов определены в spi_clock_source_t. По умолчанию драйвер установит spi_device_handle_t::cfg::clock_source в значение SPI_CLK_SRC_DEFAULT. Это обычно означает самую высокую частоту среди источников тактирования GPSPI. Реальное значение тактовой частоты при этом зависит от используемой модели чипа.

Реальная тактовая частота устройства может быть не точно равна установленному вами значению, она будет пересчитана драйвером для достижения самого ближайшего, аппаратно совместимого значения, и не больше, чем частота источника тактов. Вы можете вызвать spi_device_get_actual_freq(), чтобы узнать реальную частоту тактов SPI, вычисленную драйвером.

Теоретический максимум скорости передачи для фазы Write/Read может быть вычислен по следующей таблице:

Количество сигналов данных Скорость (bps)
1-Line Частота тактов SPI / 8
2-Line Частота тактов SPI / 4
4-Line Частота тактов SPI / 2

Вычисление скорости передачи других фаз (command, address, dummy) аналогичное.

Промахи кэша. Конфигурация по умолчанию помещает в IRAM только код ISR. Другие связанные с SPI функции, включая сам драйвер и callback, могут быть подвержены промахам кэша, потому что выполнение кода будет приостанавливаться на подкачку инструкций из flash. Для того, чтобы весь драйвер SPI поместить в IRAM, выберите CONFIG_SPI_MASTER_IN_IRAM, и поместите свои callback-функции и код, который они вызывают, также в IRAM, это предотвратит промахи кэша и связанные с этим задержки.

Примечание: поскольку реализация драйвера SPI основана на FreeRTOS API, для использования CONFIG_SPI_MASTER_IN_IRAM вы не должны разрешать CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH.

Для interrupt-транзакцйи общая цена в микросекундах составит 20+8n/Fspi на одну трназакцию (здесь n это количество байт, Fspi частота тактов SPI в МГц). Соответственно скорость передачи в МГц составит n/(20+8n/Fspi). Пример скорости передачи на тактовой частоте 8 МГц SCLK показан в следующей таблице.

Интервал транзакции, мкс Длина транзакции, байт Общее время, мкс Общая скорость, KBps
25 1 26 38.5
8 33 242.4
16 41 490.2
64 89 719.1
128 153 836.6

Когда длина данных транзакции невысока, повышается цена интервала транзакции. Если это возможно, попробуйте слить несколько коротких транзакций в одну, чтобы достичь самой высокой скорости передачи.

Обратите внимание, что по умолчанию ISR запрещен во время операций с flash. Чтобы сохранить отправку во время операций с flash, разрешите CONFIG_SPI_MASTER_ISR_IN_IRAM и установите ESP_INTR_FLAG_IRAM в поле spi_bus_config_t::intr_flags. В этом случае все транзакции, поставленные в очередь перед началом операций с flash, будут обработаны ISR параллельно. Также имейте в виду, что callback-функция каждого устройства и вызываемые из неё функции должны находиться в IRAM, или ваша callback-функция упадет из-за промаха кэша. Для дополнительной информации см. врезку "Конкурентный доступ к flash на SPI1" из статьи [5].

Пример приложения. Пример кода, использующего полудуплексный режим SPI master для чтения/записи AT93C46D EEPROM (8-битный режим) можно найти в директории peripherals/spi_master/hd_eeprom среди примеров ESP-IDF.

Выбор режима SPI (CPHA и CPOL). У библиотеки драйвера SPI Master в системе программирования ESP-IDF есть одна неприятная особенность - невозможность управлять начальным и конечным состоянием сигнала данных - это состояние всегда лог. 1, лог. 0 сделать нельзя. Таким образом, нельзя выполнить полноценную инверсию сигнала данных, управляя настройками CPHA и CPOL (задаются полем mode структуры конфигурации spi_device_interface_config_t). С подобной проблемой пользователи библиотеки уже сталкивались [6].

[Справочник по API]

В таблицах ниже приведено общее описание API драйвера SPI Master. Полное описание структур, типов, перечислений и макросов см. в документации [1].

Общие определения. Заголовочный файл components/hal/include/hal/spi_types.h.

Компонент драйвера. Описание функций компонента находится в файле components/driver/spi/include/driver/spi_common.h.

Функция Описание
spi_bus_initialize Инициализирует шину SPI. Предупреждение: SPI0 и SPI01 не поддерживаются. Если выбран канал DMA, то любой буфер для приема или передачи должен находится в DMA-capable памяти (т. е. в IRAM). ISR драйвера SPI всегда выполняется на ядре, которая вызовет эту функцию. Никогда не тратьте процессорное время для ISR на этом ядре, иначе транзакции SPI не будут обработаны.
spi_bus_free Освободит шину SPI. Предупреждение: чтобы вызов этой функции завершился успешно, все устройства сначала должны быть удалены.

SPI Master. Заголовочный файл components/driver/spi/include/driver/spi_master.h.

Функция Описание
spi_bus_add_device Выделяет на шине SPI подчиненное устройство. Вызов этой функции инициализирует внутренние структуры для устройства, плюс выделяется вывод выборки CS на указанном периферийном устройстве SPI master, и направляет этот сигнал выборки на указанный порт GPIO. У всех устройств SPI master есть 3 сигнала CS, и таким образом они могут управлять несколькими устройствами, подключенными к шине - от 1 до 3 устройств. Примечание: хотя в общем случае поддерживаются скорости до 80 МГц на выделенных выводах SPI и до 40MHz на выводах, перенаправленных через GPIO matrix, полнодуплексные передачи, направленные через GPIO matrix, поддерживают скорости только до 26 МГц.
spi_bus_remove_device Удалит устройство с шины SPI.
spi_device_queue_trans Ставит SPI-транзакцию SPI в очередь для выполнения как interrupt-транзакцию. Результат транзакции извлекается вызовом spi_device_get_trans_result. Примечание: обычно устройство не может запустить (поставить в очередь) одновременно polling-транзакции и interrupt-транзакции.
spi_device_get_trans_result Извлекает результат SPI-транзакции, которая была ранее поставлена в очередь вызовом spi_device_queue_trans. Эта подпрограмма будет ждать, пока транзакция для указанного устройства не будет успешно выполнена. Функция возвратит описание завершенной транзакции, чтобы программа могла проверить результат, и по этому результату выполнить нужные действия, например освободить память, или повторно исопльзовать буферы для других транзакций.
spi_device_transmit Посылает SPI-транзакцию с ожидением её завершения, и возвратит результат транзакции. Эта функция эквивалентна последовательным вызовам функций spi_device_queue_trans() и spi_device_get_trans_result(). По этой причине не используйте spi_device_transmit, кода существует транзакция, которая была отдельно поставлена в очередь (запущена) из вызова spi_device_queue_trans() или polling_start/transmit, и которая еще не завершена. Примечание: функция spi_device_transmit не является потокобезопасной (not thread safe), когда несколько задач обращаются к одному устройству SPI. Обычно устройство не может запустить (поставить в очередь) одновременно polling-транзакции и interrupt-транзакции.
spi_device_polling_start Немедленно запустит polling-транзакцию. Примечание: обычно устройство не может запустить (поставить в очередь) одновременно polling-транзакции и interrupt-транзакции. Кроме того, устройство не может запустить новую polling-транзакцию, если другая polling-транзакция не была завершена.
spi_device_polling_end Производит опрос в ожидании завешнения polling-транзакции. Эта подпрограмма не выполнит возврат, пока не будет успешно завершена транзакция для указанного устройства. Задача не блокируется, однако активно крутится в цикле, пока транзакция не завершится.
spi_device_polling_transmit Посылает polling-транзакцию, ждет её завершения и возвратит результат. Эта функция эквивалентна последовательным вызовам spi_device_polling_start() и spi_device_polling_end(). Не используйте spi_device_polling_transmit, когда имеется работающая, все еще не завершенная транзакция SPI. Примечание: эта функция не является потокобезопасной, когда несклько задач обращаются к одному и тому же устройству SPI. Обычно устройство не может запустить (поставить в очередь) одновременно polling-транзакции и interrupt-транзакции.
spi_device_acquire_bus Оккупирует шину SPI для устройства, чтобы выполнить непрерывно следующие друг за другом транзакции. Транзакции для всех других устройств будут выключены до тех пор, пока не будет вызвана функция spi_device_release_bus. Примечание: эта функция будет ждать, пока не будут отправлены все существующие транзакции.
spi_device_release_bus Освободит шину SPI, оккупированную для устройства. Все другие устройства после этого смогут запускать SPI-транзакции.
spi_device_get_actual_freq Вычислит рабочую частоту тактов для указанного устройства.
spi_device_get_actual_freq Вычислит рабочую частоту тактов для указанного устройства.
spi_get_actual_clock Вычислит рабочую частоту, которая самая близкая к указанной частоте.
spi_get_timing Вычислит установки тайминга указанных частоты и настроек. Примечание: если **dummy_o* не 0, то это означет, что dummy-биты должны быть применены в полудуплексном режиме, и полнодуплексный режим может не работать.
spi_get_freq_limit Извлечет предельную частоту для текущей конфигурации. SPI master на этой предельной частоте сможет нормально работать, в то время как выше этого предела режим полного дуплекса и DMA работать не будут, и dummy-биты будут применяться в полудуплексном режиме.

[Ссылки]

1. ESP32-C3 SPI Master Driver site:espressif.com.
2. ESP32-C3: использование выводов SPI flash как обычных портов GPIO.
3. ESP32-C3: справочник по выводам.
4. Порядок следования байт (endianness).
5. ESP32: SPI Flash API.
6. Issues with SPI Mode 3 on ESP32 using ESP_IDF site:esp32.com.