Отправка и прием данных неизвестной длинны по UART через DMA в freeRTOS на STM32 с использованием LL

Вступление

Всем доброго времени суток! В заголовке данной статьи как раз написано то, о чем я хотел бы Вам рассказать, только добавим немного конкретики об использованном стеке:

  • Камень STM32F103C8

  • CubeMX 6.2.1

  • IAR 8.30.1

  • FreeRTOS 10.0.1, CMSIS v2

  • Все периферийные драйвера LL

Для написания и отладки прошивки был собран небольшой стенд (картинка ниже). На стенде используется платка Bluepill, программатор ST-Link v2, два USB-UART переходника на CP2102 и несколько выводных отладочных светодиодов (куда без них?).

Стенд для тестирования
Стенд для тестирования

Основная часть

Микроконтроллер был настроен следующим образом (смотреть картинку ниже), выбрана максимальная частота работы.

Конфигурация ножек микроконтроллера
Конфигурация ножек микроконтроллера

Описание буду проводить на примере USART2. Для начала сконфигурируем его как на картинках ниже. Выбираем стандартные параметры, добавляем 6 и 7 DMA каналы, разрешаем глобальное прерывание от USART2.

Настройки USART2
Настройки USART2
Добавим RX и TX DMA запросы
Добавим RX и TX DMA запросы
Разрешим прерывание от DMA и глобальное прерывание
Разрешим прерывание от DMA и глобальное прерывание

Перейдем к настройке freeRTOS. Выбираем версию интерфейса CMSIS_v2. Теперь добавим очереди RX/TX, потоки для их обработки и одно событие. Для моей задачи нужно было принимать и отправлять данные в виде ASCII строк, с максимальной длинной в 128 байтов (с учетом '\0'). Событие нам потребуется для определения состояния DMA в потоке передачи данных.

Создадим очередь для сообщений на отправку
Создадим очередь для сообщений на отправку
Создадим очередь для принятых сообщений
Создадим очередь для принятых сообщений
Создадим задачу отправки данных
Создадим задачу отправки данных
Создадим задачу для обработки принятых данных
Создадим задачу для обработки принятых данных
Создадим событие для USART2
Создадим событие для USART2

С настройкой в CubeMX закончили, перейдем к коду.

В main.h создаем макросы:

#define UART2_TX_LENGTH 128
#define UART2_RX_LENGTH 128

/*
	Выставляется, когда 7 канал DMA завершит работу.
  То есть когда все отправляемые данные будут переданы USART периферии.
  Это будет сигналом о том, что можно запускать передачи следующих данных.
*/
#define UART2_event_tx_dma_complete 0x00000001U

В main.c:

/* 
	Обработчик будет вызван в случае обнаружения IDLE состояния RX линии USART.
  Это будет означать, что завершилась транзакция приема данных и бизнес логика
  приложения может их забрать.
*/
void USART2_IdleCallback()
{
  extern char UART2_rx[UART2_RX_LENGTH];
  extern osMessageQueueId_t rxDataUART2Handle;

  uint32_t length = UART2_RX_LENGTH - LL_DMA_GetDataLength(DMA1, LL_DMA_CHANNEL_6);         
  if (length < UART2_RECEIVE_LENGTH) {
    UART2_rx[length] = 0; //Добавляем '\0' в конец строки
    osMessageQueuePut(rxDataUART2Handle, UART2_rx, 0, 0);
  } else {
    //Overflow rx data
  }

  LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_6);
  LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_6, UART2_RX_LENGTH);
  LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_6);
}

В usart.c добавляем следующий код:

/* USER CODE BEGIN 0 */
char UART2_tx[UART2_TX_LENGTH];
char UART2_rx[UART2_RX_LENGTH];
/* USER CODE END 0 */
/* USER CODE BEGIN USART2_Init 2 */
//Настройка 6 канала DMA (прием данных)
LL_USART_EnableIT_IDLE(USART2);
LL_DMA_ConfigAddresses(DMA1, LL_DMA_CHANNEL_6, LL_USART_DMA_GetRegAddr(USART2),  (uint32_t)&UART2_rx, LL_DMA_GetDataTransferDirection(DMA1, LL_DMA_CHANNEL_6)););
LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_6, UART2_RX_LENGTH);
LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_6);
LL_USART_EnableDMAReq_RX(USART2);

//Настройка 7 канала DMA (передача данных)
LL_DMA_EnableIT_TC(DMA1, LL_DMA_CHANNEL_7);
LL_USART_EnableDMAReq_TX(USART2);
/* USER CODE END USART2_Init 2 */

В файле stm32f1xx_it.c находим прерывание 6 и 7 каналов DMA и вставляем следующий код:

/* USER CODE BEGIN Includes */
#include "cmsis_os.h"
/* USER CODE END Includes */
/* USER CODE BEGIN DMA1_Channel6_IRQn 0 */
if(LL_DMA_IsActiveFlag_TC6(DMA1))
{
  LL_DMA_ClearFlag_TC6(DMA1);
}
/* USER CODE END DMA1_Channel6_IRQn 0 */
  /* USER CODE BEGIN DMA1_Channel7_IRQn 0 */
  extern osEventFlagsId_t UART2_EventsHandle;
  if(LL_DMA_IsActiveFlag_TC7(DMA1))
  {
    LL_DMA_ClearFlag_TC7(DMA1);
    LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_7);
    
    //Выставляем флаг завершения передачи данных DMA периферии USART
    osEventFlagsSet(UART2_EventsHandle, UART2_event_tx_dma_complete);
  }
  /* USER CODE END DMA1_Channel7_IRQn 0 */

В файле freeRTOS.c добавляем код наших задач:

void StartParseUART2DataTask(void *argument)
{
  /* USER CODE BEGIN StartParseUART2DataTask */
  char data[UART2_RX_LENGTH];
 
/* Infinite loop */
  for(;;)
  {
    //Ожидаем принятых данных. Пока ждем - данная задача в состоянии BLOCKED  
    osMessageQueueGet(rxDataUART2Handle, data, NULL, osWaitForever);
    
    /*
    	Работа с data
    */
  }
void StartSendUART2Task(void *argument)
{
  /* USER CODE BEGIN StartSendUART2Task */
  char data[UART2_TX_LENGTH];

  /* Infinite loop */
  for(;;)
  {
    //Ждем сообщения из очереди. Пока ждем - данный поток BLOCKED
    osMessageQueueGet(txDataUART2Handle, data, NULL, osWaitForever);  
    
    //Ждем выставления флага о завершении передачи DMA. Пока ждем - данный поток BLOCKED
    osEventFlagsWait(UART2_EventsHandle, UART2_event_tx_dma_complete, osFlagsWaitAny, osWaitForever);

    LL_DMA_ConfigAddresses(DMA1, LL_DMA_CHANNEL_7, (uint32_t)&str, LL_USART_DMA_GetRegAddr(USART2), LL_DMA_GetDataTransferDirection(DMA1, LL_DMA_CHANNEL_7)    );
    LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_7, strlen(data));
    LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_7); 
  }
  /* USER CODE END StartSendUART2Task */
}
/* USER CODE BEGIN RTOS_EVENTS */
//Выставляем флаг завершения передачи DMA при инициализации, иначе не передадим первое сообщение      пе
osEventFlagsSet(UART2_EventsHandle, UART2_event_tx_dma_complete);
/* USER CODE END RTOS_EVENTS */

Теперь можно собирать проект и заливать прошивку в наш камень.

Логика работы

Прием данных

При запуске устройства мы сразу же инициализируем прием данных по DMA. Выставляем количество байт, которые нужно принять равное максимально возможному количеству. Разрешаем IDLE прерывании линии RX USARTx. Таким образом, данные которые поступают в устройство, с помощью DMA перемещаются в указанный нами буфер. Когда поступление данных в устроство закончилось, на линии RX USARTx выставляется и удерживается высокий уровень. Если высокий уровень удерживается в течении времени приема одного байта, то периферии USARTx вызывает прерывание IDLE Line detection. В этот момент мы переходим в обработчик прерывания USART2_IdleCallback, в котором мы заносим данные из буфера в очередь. Также у нас есть поток обработки принятых данных. В нем мы просто ожидаем появления елемента в очереди. Когда он появляется - поток выходит из состояния BLOCKED и мы можем обрабатывать данные.

Передача данных

Для передачи данных по USARTx через DMA: выставляем флаг разрешения передачи через DMA и разрешаем прерывание по завершению передачи. При инициализции freeRTOS выставляем события завершения передачи по DMA для передачи первого сообщения. В потоке, который обслуживает передачу по USARTx, мы ждем появления данных в очереди, затем ждем флаг, и после этого запускаем передачу. Когда передача по DMA будет завершена - произойдет вызов обработчика события DMA TC, в котором мы очистим флаг TC и выставим флаг события для отправки следующих данных.

Завершение

На этом пожалуй все, надеюсь эта информация была Вам полезна!