Продолжаем развивать тему STM32CubeMx! Речь в этой статье пойдет о совместном использовании модулей STM32 SPI и DMA.
Давайте чуть усложним задачу и задействуем 4 модуля SPI, ну и, соответственно, нам понадобятся 4 канала DMA, по одному на каждый модуль SPI. Я буду использовать отладочную плату STM32F429Discovery, но никаких особенностей к процессу это не добавит, так что те, кто используют другую плату с другим микроконтроллером, просто не забывайте указать это при создании проекта (при выборе МК).
Проект мы создали, теперь выбираем и активируем нужные нам модули:
Cube сразу же «забронировал» 12 выводов контроллера для сигналов SPI:
- SPI1_SCK
- SPI1_MOSI
- SPI1_MISO
- SPI2_SCK
- SPI2_MOSI
- SPI2_MISO
- SPI3_SCK
- SPI3_MOSI
- SPI3_MISO
- SPI4_SCK
- SPI4_MOSI
- SPI4_MISO
Переходим на вкладку Configuration:
И выбираем SPI1. В появившемся окне нас интересует пункт DMA Settings. Там мы можем, нажав на кнопку Add, добавить использование DMA для приема и передачи данных по SPI. Давайте для примера ограничимся передачей:
И, кроме того, давайте включим глобальное прерывание DMA, это нам понадобится для того, чтобы понимать, когда можно запускать новую отправку:
Больше никакие настройки для SPI1 менять не будем. Понятно, что в зависимости от устройства, с которым предстоит наладить общение, необходимо также настраивать параметры передачи, такие как скорость и т. д. Теперь производим аналогичные действия для трех оставшихся модулей SPI и просим Cube сгенерировать для нас проект 🙂
Давайте рассмотрим некоторые из полученных функций на примере все того же модуля SPI1. Вот так выглядит инициализация непосредственно SPI:
void MX_SPI1_Init(void) { hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi1.Init.TIMode = SPI_TIMODE_DISABLED; hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLED; HAL_SPI_Init(&hspi1); }
Настройка нужного канала DMA осуществляется в функции HAL_SPI_MspInit():
hdma_spi1_tx.Instance = DMA2_Stream3; hdma_spi1_tx.Init.Channel = DMA_CHANNEL_3; hdma_spi1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_spi1_tx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_spi1_tx.Init.MemInc = DMA_MINC_ENABLE; hdma_spi1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_spi1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_spi1_tx.Init.Mode = DMA_NORMAL; hdma_spi1_tx.Init.Priority = DMA_PRIORITY_VERY_HIGH; hdma_spi1_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE; HAL_DMA_Init(&hdma_spi1_tx); __HAL_LINKDMA(hspi,hdmatx,hdma_spi1_tx);
Прерывания как обычно находятся в файле stm32f4xx_it.c (если вы используете другой контроллер, то и файл может называться иначе, к примеру, stm32f1xx_it.c). Как вы помните, мы активировали прерывания для каждого модуля DMA. Давайте добавим в нашу программу 4 переменные-флага, которые будут символизировать статус текущей отправки для каждого из используемых модулей. Добавим в main.c:
uint8_t completed1 = 1; uint8_t completed2 = 1; uint8_t completed3 = 1; uint8_t completed4 = 1;
Если значение переменной равно 1, то передача завершена, если 0, то передача в данный момент продолжается. Таким образом, нам нужно перед отправкой обнулять флаг, а в прерывании по завершению передачи ставить его равным 1. Так и поступим:
Важное замечание!
В каждом из обработчиков прерываний DMA необходимо проверять, чем именно было вызвано прерывание. В данном примере нас интересует только прерывание по окончанию передачи, но помимо этого случая прерывание может быть вызвано другими событиями:
- окончание передачи
- передача половины данных
- ошибка передачи
- ошибка Direct mode
- ошибка FIFO
Для простоты примера мы эту проверку опускаем, но в «боевых» проектах обязательно проверяйте флаги!
Кроме того, можно использовать специально созданные разработчиками HAL callback-функции. Тут идея такая — в функции HAL_DMA_IRQHandler (определенной в библиотеке HAL) уже происходит проверка, каким именно событием вызвано прерывание, и в соответствии с этим будет вызвана соответствующая callback-функция. Таким образом, нам не потребуется проверять, какое именно событие вызвало прерывание, вручную.
/** * @brief This function handles DMA2 Stream3 global interrupt. */ void DMA2_Stream3_IRQHandler(void) { /* USER CODE BEGIN DMA2_Stream3_IRQn 0 */ completed1 = 1; /* USER CODE END DMA2_Stream3_IRQn 0 */ HAL_DMA_IRQHandler(&hdma_spi1_tx); /* USER CODE BEGIN DMA2_Stream3_IRQn 1 */ /* USER CODE END DMA2_Stream3_IRQn 1 */ } /** * @brief This function handles DMA2 Stream1 global interrupt. */ void DMA2_Stream1_IRQHandler(void) { /* USER CODE BEGIN DMA2_Stream1_IRQn 0 */ completed4 = 1; /* USER CODE END DMA2_Stream1_IRQn 0 */ HAL_DMA_IRQHandler(&hdma_spi4_tx); /* USER CODE BEGIN DMA2_Stream1_IRQn 1 */ /* USER CODE END DMA2_Stream1_IRQn 1 */ } /** * @brief This function handles DMA1 Stream4 global interrupt. */ void DMA1_Stream4_IRQHandler(void) { /* USER CODE BEGIN DMA1_Stream4_IRQn 0 */ completed2 = 1; /* USER CODE END DMA1_Stream4_IRQn 0 */ HAL_DMA_IRQHandler(&hdma_spi2_tx); /* USER CODE BEGIN DMA1_Stream4_IRQn 1 */ /* USER CODE END DMA1_Stream4_IRQn 1 */ } /** * @brief This function handles DMA1 Stream5 global interrupt. */ void DMA1_Stream5_IRQHandler(void) { /* USER CODE BEGIN DMA1_Stream5_IRQn 0 */ completed3 = 1; /* USER CODE END DMA1_Stream5_IRQn 0 */ HAL_DMA_IRQHandler(&hdma_spi3_tx); /* USER CODE BEGIN DMA1_Stream5_IRQn 1 */ /* USER CODE END DMA1_Stream5_IRQn 1 */ }
Все приготовления завершены, давайте уже наконец-то что-нибудь отправим. Объявляем массив тестовых данных:
uint8_t testDataToSend[128];
Заполняем массив данными:
for (uint8_t i = 0; i < 128; i++) { testDataToSend[i] = i + 1; }
И вызываем функцию отправки, не забыв перед этим обнулить флаг готовности передачи:
completed1 = 0; HAL_SPI_Transmit_DMA(&hspi1, testDataToSend, 128);
Передача данных через любой из оставшихся модулей SPI выглядит абсолютно аналогично:
completed2 = 0; HAL_SPI_Transmit_DMA(&hspi2, testDataToSend, 128);
Нам остается только проверять флаг окончания передачи и в случае, если он стал равным единице, мы можем запустить повторную передачу, либо начать передавать уже другие данные.
Как видите, все оказалось довольно просто, STM32CubeMx сгенерировал нужный нам код инициализации всех периферийных модулей, мы же дописали верхний уровень программы так, как нам это было нужно. На этом я с вами прощаюсь, до новых статей! 🙂