STM32Cube. Использование SPI и DMA.

Продолжаем развивать тему STM32Cube при работе с микроконтроллерами STM32! Речь в этой статье пойдет о совместном использовании модулей SPI и DMA.

SPI и DMA

Давайте чуть усложним задачу и задействуем 4 модуля SPI, ну и, соответственно, нам понадобятся 4 канала DMA, по одному на каждый модуль SPI.

Я буду использовать отладочную плату STM32F429-Discovery, но никаких особенностей к процессу это не добавит, так что те, кто используют другую плату с другим микроконтроллером, просто не забывайте указать это при создании проекта (при выборе МК).

Проект мы создали, теперь выбираем и активируем нужные нам модули:

Настройка SPI

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

И, кроме того, давайте включим прерывание по окончанию передачи данных по 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). Как вы помните, мы для каждого модуля SPI активировали прерывания по завершению передачи данных через DMA. Давайте добавим в нашу программу 4 переменные-флага, которые будут символизировать статус текущей отправки для каждого из используемых модулей. Добавим в main.c:

uint8_t completed1 = 1;
uint8_t completed2 = 1;
uint8_t completed3 = 1;
uint8_t completed4 = 1;

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

/**
* @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);

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

Как видите, все оказалось довольно просто, STM32Cube сгенерировал нужный нам код инициализации всех периферийных модулей, мы же дописали верхний уровень программы так, как нам это было нужно. На этом я с вами прощаюсь, до новых статей! 😉

Понравилась статья? Поделись с друзьями!

STM32Cube. Использование SPI и DMA.: 31 комментарий
  1. Здравствуйте! Спасибо огромное за ваши полезнейшие уроки! Скажите, можно ли рассчитывать на подобный урок с hal+cube, с использованием I2C? Желательно с памятью.

    • Добрый вечер! Да, планируется статья по I2C, но пока сложно сказать как скоро… И точно можно сказать, что будет акселерометр, но в принципе думаю с памятью не возникнет проблем.

  2. Буду ждать с нетерпением. Вот с памятью как раз и проблема, никак не могу заставить работать 24с02. функция HAL_I2C_Mem_Read(&hi2c1, 0x50, 0x00, 1, &receive, 1, 500); не пишет, а HAL_I2C_IsDeviceReady(&hi2c1, 0x50, 20, 500) == HAL_ERROR верно сразу.
    прошу прощения за оффтоп, может вы это учтете или поможет кто-нибудь.

  3. уже нафлудил немало, в общем все работает, надо было адрес сместить на 1 влево, извиняйте)

  4. Да, спасибо огромное за материалы! Как раз сейчас с этим разбирался, прямо угадали!
    Скажите, успели ли Вы поработать с STM32F7? Насколько они лучше F4 в плане DSP? Или еще сырой материал?

  5. Я пробовал STM32F7.
    То же самое что и F4 по периферии. только есть кое где дополнительные режимы. Отличия в основном в ядре. Тесты показали, что ядро быстрее примерно в 1.5 раза при одинаковых частотах, благодаря кешу, который, кстати, нужно еще и включать.

  6. Очень интересные статьи, но не могли бы вы к статьям прикладывать ваши рабочие проекты. По готовому проекту проще разбираться.

  7. Статья планируется с использованием DMA? Если да, то тоже буду ждать с нетерпением.

  8. Добрый день.
    В этой статье рассмотрели SPI. Хочу коснуться вопроса про его спутника — интерфейс I2S.
    Есть ли какая-то статья про его использование? И планируется ли она?

    Сейчас осваиваю данный интерфейс и столкнулся с неожиданной проблемой при использовании DMA.

    Мой пример такой: пытаюсь передать синус по I2S, используя полнодуплексный режим с ПДП. Плата STM32F3Discovery. Замкнул ножки I2S2_SD и I2S2_extSD. И пересылаю данные из одно буфера другой. При однократной передаче проблем не возникает, однако стоит добавить функцию HAL_I2SEx_TransmitReceive_DMA в бесконечный цикл и всё… Передача осуществляется только один раз, а если пробежаться по отладчику, то функция ничего не принимает и не передает и возвращает состояние HAL_BUSY.

    Может надо как-то останавливать передачу после приема данных или же буфер-приемник каким-то образом очищать?

    • А если вызывать HAL_I2SEx_TransmitReceive_DMA() раз в секунду к примеру или еще реже, но периодически?

  9. Поставил таймер. 1 мс оказалось достаточно, чтобы передача успела завершиться.

    Есть ещё вопрос по DMA. Обработчик прерывания в DMA, при использовании стандартной HAL функции, вызывается по окончании передачи в память целого буфера данных или его половины?

  10. у меня при передаче по UART через DMA возникает прерывание по приему — проверял флаг
    void USART1_IRQHandler(void)
    {
    /* USER CODE BEGIN USART1_IRQn 0 */
    int recieve = __HAL_UART_GET_IT_SOURCE(&huart1, UART_IT_RXNE);
    /* USER CODE END USART1_IRQn 0 */
    HAL_UART_IRQHandler(&huart1);
    /* USER CODE BEGIN USART1_IRQn 1 */
    if (recieve)
    {
    IndexInBuf1++;
    HAL_UART_Receive_IT(&huart1, &Uart1InBuffer[IndexInBuf1], 1);
    }
    /* USER CODE END USART1_IRQn 1 */
    }
    и принимал неверный байт. При отправке по DMA разве должно быть прерывание по приему?

    • По идее прерывание по приему не должно срабатывать при передаче. Можно попробовать чуть иначе проверять:
      tmp1 = __HAL_UART_GET_FLAG(huart, UART_FLAG_RXNE);
      tmp2 = __HAL_UART_GET_IT_SOURCE(huart, UART_IT_RXNE);

      if((tmp1 != RESET) && (tmp2 != RESET))
      {
      UART_Receive_IT(huart);
      }

  11. проверка флага о том, что в приемном буфере есть что то помогла, но тогда получается что при отправке по пдп происходит ложное прерывание. Нужно errata читать.

  12. Переменные completed не видны в файле stm32f4xx_it.c — они объявлены в main.c

    Получается, их надо объявить в stm32f4xx_it.c с модификатором extern?

  13. пытаюсь реализовать подобное с Усартом. На прием без проблем, а на передача только один раз, потом прерывание не срабатывает. В чем может быть проблема?

    • Скоро будет статья про Cube и I2C, правда скорее всего без DMA. Но в Cube очень просто будет по аналогии с этой статьей добавить DMA.

  14. Добрый день. Не нашел в данной статье ничего про CS. Так понял, NSS в режиме мастера не работает. Надеюсь, я правильно понял, что нужно как то ловить прерывания SPI и выставлять бит CS на определенное время. Или это бесполезно и нужно каждый новый байт конфигурировать отдельно?

    • Я обычно делаю CS на отдельном пине контроллера и управляю им перед отправкой и после отправки полной посылки.

    • Проверил, в режиме SPI Master аппаратный NSS не работает. Поэтому нужно ставить его программно. И кстати, если необходимо вывести сигнал на ЦАП, то очень помогает ШИМ в качестве формирователя CS. Данный трюк позволяет несколько снизить нагрузку на ЦП.

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

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