Всех снова рад приветствовать, сегодня продолжаем шествовать по периферийным модулям микроконтроллеров STM32, очередной герой - интерфейс SPI. Не будем изменять традициям, пробежимся по некоторым теоретическим моментам и перейдем к созданию реального примера.
Теоретическая часть.
Интерфейс SPI.
Приступим! Интерфейс SPI позволяет связать между собой два и более устройств. Большой плюс SPI - быстродействие, так что большой объем данных улетит легко. Но в SPI, в отличие, например от I2C, для подключения N устройств потребуется большее количество линий (N + 3), а не 2, как в I2C. Так что, как и во всем, есть и плюсы и минусы, которые просто необходимо иметь ввиду в каждом конкретном проекте и в конкретной же задаче.
Существует несколько типов подключения к шине, но в общем-то, алгоритм работы при любом подключении практически один и тот же. Ведущий генерирует тактовый сигнал с вывода CLK и синхронно с этим сигналом передает данные по линии MOSI. В то же время подчиненное устройство передает данные в обратном направлении по линии MISO. Получается, что все сыты и довольны 👍 Хотя используется также подключение, при котором подчиненный только кушает байты данных, а сам ничего не шлет. А при подключении нескольких устройств возможно два варианта - независимое и каскадное. При независимом требуется больше линий, но такое подключение используется чаще.
Что же нам предлагает в плане SPI STM32?
- Возможно использование контроллера, как в качестве ведущего, так и в качестве подчиненного (это и так очевидно).
- Формат кадра - 8 или 16 бит.
- Возможность работы в режиме MultiMaster.
- Наличие огромного количества разных флагов - как для индикации окончания приема и передачи, так и для отлавливания разнообразных ошибок.
- Соответствующие прерывания.
- Возможна работа с использованием DMA.
- Аппаратное управление пином NSS для выбора подчиненного.
В общем, все, что требуется, присутствует. Список прерываний для SPI:
Практическая часть.
Библиотека SPL.
Здесь в оригинальной статье у меня шло описание, относящееся к библиотеке SPL, которая на текущий момент уже не поддерживается. Так что оставлю под спойлером исключительно ностальгических воспоминания ради )
Давайте посмотрим, как можно настроить SPI для работы в нужном режиме. Как и раньше мы будем использовать Standard Peripheral Library. Лезем в библиотеку, находим и открываем файл stm32f10x_spi.h. Прямо в начале файла все, что нам понадобится:
typedef struct { uint16_t SPI_Direction; uint16_t SPI_Mode; uint16_t SPI_DataSize; uint16_t SPI_CPOL; uint16_t SPI_CPHA; uint16_t SPI_NSS; uint16_t SPI_BaudRatePrescaler; uint16_t SPI_FirstBit; uint16_t SPI_CRCPolynomial; }SPI_InitTypeDef;
Назначив всем этим полям структуры SPI_InitTypeDef
определенные значения, мы настраиваем модуль SPI. Тут вроде бы все понятно, но давайте по традиции разберем для чего нужно каждое отдельное поле.
uint16_t SPI_Direction
- направление передачи данных, возможные значения:
SPI_Direction_2Lines_FullDuplex SPI_Direction_2Lines_RxOnly SPI_Direction_1Line_Rx SPI_Direction_1Line_Tx
uint16_t SPI_Mode
- режим работы, подчиненный или ведущий (master или slave).uint16_t SPI_DataSize
- DataSize и этим все сказано, размер данных - 8 или 16 бит.uint16_t SPI_CPOL
,uint16_t SPI_CPHA
- а это настройки тактового сигнала.uint16_t SPI_NSS
- тут мы выбираем, как будет управляться сигнал NSS - аппаратно или программно. Соответственно возможные значения поля:
SPI_NSS_Soft SPI_NSS_Hard
uint16_t SPI_BaudRatePrescaler
- предделитель частоты.uint16_t SPI_FirstBit
- здесь выбираем, с какого бита начнется передача (младшего или старшего).uint16_t SPI_CRCPolynomial
- контрольная сумма.
Все возможные значения для всех полей описаны все в том же файле stm32f10x_spi.h чуть ниже определения структуры. В другом файле из SPL - stm32f10x_spi.c - функции для работы с SPI.
Работа по SPI с STM32CubeMx и HAL.
А, соответственно, пример будем создавать с использованием уже актуального инструментария. В качестве ознакомления с SPI в STM32 создадим максимально базовый проект - просто закинем некоторое количество байт в SPI через DMA. Другие примеры, где так или иначе используется этот интерфейс, можно найти вот тут:
- Подключение дисплея на базе ST7735 к микроконтроллеру STM32.
- Дисплей на базе ST7735 и STM32. Вывод изображения.
- STM32 и Ethernet. Часть 1. Подключение и настройка ENC28J60.
Я буду использовать отладочную плату STM32F429Discovery в этот раз, но никаких особенностей к процессу это не добавит, для любого другого контроллера суть будет точно такой же.
Создаем проект и первым делом активируем модуль SPI, пусть будет SPI1, по классике:
Режим выбираем "Full-Duplex Master", и поскольку наша задача - создать простой, базовый проект - то остальные настройки можем оставить дефолтными.
CubeMx сразу же "забронировал" 3 вывода контроллера для сигналов SPI:
- SPI1_SCK
- SPI1_MOSI
- SPI1_MISO
Хотя настройки SPI мы и оставили в покое, тем не менее необходимо вернуться в соответствующий раздел и произвести настройку DMA на вкладке "DMA Settings". Нажав на "Add" добавляем канал DMA, для которого задаем:
- DMA Request - SPI1_TX, поскольку будем использовать его для передачи данных
Результат выглядит следующим образом:
Кликнув на добавленный канал, получаем доступ к его настройкам:
Соответственно, здесь у нас выбрано:
- режим работы - Normal
- инкрементирование адреса в периферии отключено, поскольку адрес регистра данных SPI1 всегда один и тот же
- а для памяти инкрементирование включено
- размер данных - один байт
Прерывания DMA будут включены автоматически, можно просто для надежности проверить выполнение данного пункта на вкладке "NVIC Settings". На этом с настройкой заканчиваем, генерируем проект и открываем его в выбранной IDE. Вся инициализация, как обычно, была сгенерирована автоматически, можно сразу переходить к сути дела.
Для того, чтобы отследить, когда передача данных завершена, используем соответствующую callback-функцию:
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
Переопределяем данную функцию в main.c:
/* USER CODE BEGIN 4 */ void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI1) { // Передача завершена } } /* USER CODE END 4 */
HAL_SPI_TxCpltCallback()
будет вызвана автоматически по окончанию процесса передачи. Далее проверяем, что событие произошло именно для нашего модуля SPI1 и в случае необходимости добавляем некие действия, которые надо выполнить после передачи.
Итак, остается только непосредственно осуществить многократно упомянутую отправку данных. Для этого объявим массив тестовых данных (transmitBuffer[]
) и зададим его размер (BUFFER_SIZE
), передачу производим при помощи:
HAL_SPI_Transmit_DMA()
И итогом будет следующий, весьма тривиальный, код:
/* Private define ------------------------------------------------------------*/ /* USER CODE BEGIN PD */ #define BUFFER_SIZE 32 /* USER CODE END PD */ /* Private macro -------------------------------------------------------------*/ /* USER CODE BEGIN PM */ /* USER CODE END PM */ /* Private variables ---------------------------------------------------------*/ SPI_HandleTypeDef hspi1; DMA_HandleTypeDef hdma_spi1_tx; /* USER CODE BEGIN PV */ uint8_t transmitBuffer[BUFFER_SIZE]; /* USER CODE END PV */ /* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_DMA_Init(void); static void MX_SPI1_Init(void); /* USER CODE BEGIN PFP */ /* USER CODE END PFP */ /* Private user code ---------------------------------------------------------*/ /* USER CODE BEGIN 0 */ /* USER CODE END 0 */ /** * @brief The application entry point. * @retval int */ int main(void) { /* USER CODE BEGIN 1 */ /* USER CODE END 1 */ /* MCU Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_DMA_Init(); MX_SPI1_Init(); /* USER CODE BEGIN 2 */ for (uint8_t i = 0; i < BUFFER_SIZE; i++) { transmitBuffer[i] = i + 1; } /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ HAL_SPI_Transmit_DMA(&hspi1, transmitBuffer, BUFFER_SIZE); HAL_Delay(1000); } /* USER CODE END 3 */ }
Отправляем в данном случае циклически, раз в секунду. По окончанию процесса, как уже обсудили, будет вызван callback, в котором можно выставить, к примеру, флаг готовности к следующей передаче. Это уже зависит от задачи, здесь просто исходим из того, что за секунду передача успела осуществиться. А создание базового примера на данном этапе завершаем 👍