STM32F103 MCU에서 UART DMA 수신 사용하기

지난번 포스팅에서 STM32F103 MCU에서 HAL Library를 사용해서 UART DMA를 사용하는 법에 대해서 살펴보았다. 이번 포스팅에서는 UART 데이터 수신에 DMA를 사용하는 법을 알아볼 것이다.

UART DMA 수신의 장점

대량의 데이터가 수신될 때 인터럽트 방식을 사용하면 매 바이트가 입려될 때마다 인터럽트 처리 루틴이 호출되기 때문에 다른 중요한 처리를 하는 데 방해가 될 수 있다. 이런 경우에 UART DMA 수신을 사용하면 아무리 큰 용량의 데이터가 들어오더라도 HT(Half Transfer) 인터럽트, TC(Transfer Completion) 인터럽트 만 발생하기 때문에 MCU 간섭을 최소화할 수 있는 장점이 있다.

UART DMA 수신시 고려사항

UART DMA 수신은 수신할 데이터 사이즈를 지정해 주어야 한다. 문제는 수신할 데이터가 고정 길이가 아니고 지정한 사이즈보다 적은 경우에 TC 인터럽트가 발생하지 않는다. 이런 경우에는 UART IDLE 인터럽트를 모니터링 함으로써 문제를 해결할 수 있다. 따라서 UART DMA 수신시에 HT, TC, IDLE 인터럽트를 모두 등록하게 되면 MCU 간섭을 최소화하면서 어떤 형태의 데이터도 수신할 수 있게된다.

UART DMA 수신 설정하기

이번 예제에서는 STM32F103ZE MCU의 UART3번과 UART4번을 사용한다.

UART3번의 Rx와 UART4번의 Tx를 서로 연결해서 UART4 포트가 DMA로 전송한 데이터를 UART3 포트가 DMA 방식으로 수신하게 한다.

먼저 “Pinout & Configuration”에서 “Connectivity->USART3″을 선택한 후, “Configuration->DMA Settings”에서 “USART3_RX”를 추가한다.

“USART3_Rx”의 “DMA Request Settins”에서 “Mode”를 “Circular”로 설정한다. Rx는 언제 데이터가 들어올지 모르기때문에 반복적인 수신처리를 할 수 있어야 하기 때문이다.

다음으로 “NVIC Settings”에서 모든 Interrupt가 “Enabled”되어 있는지 확인한다.

고속 UART 데이터 처리를 위해서 baudrate를 2Mbps로 설정한다.

UART4 포트에 대해서도 동일한 설정이 되어 있는지 확인한다.

Coding

“Generate Code”를 수행하면 main.c, stmref1xx_it.c에 새로운 코드가 추가된 것을 확인할 수 있다.

UART DMA RX의 동작 흐름을 설명하면 다음과 같다.

  • UART DMA RX를 Enable한다.
  • UART DMA RX로 데이터를 수신하는 중에 HT, TC 인터럽트가 발생한다. 해당 인터럽트 서비스 루틴에서 UART DMA RX 버퍼에 수신된 데이터를 사용자 메모리로 복사한다.
  • UART IDLE Interrupt가 발생하면 UART DMA RX 버퍼에 남아있는 유효한 데이터를 사용자 메모리로 복사한다.

위와 같은 동작을 위해서 필요한 변수를 선언한다.

uint8_t RxBuffer[MAX_RX_BUF + 16];
uint16_t wrPtr = 0;
uint16_t rdPtr = 0;
uint16_t rcvdLen = 0;
uint8_t rcvFlag = 0;
uint8_t RxBuf3[MAX_RX_BUF + 16];

uint32_t totalSentBytes = 0;
uint32_t totalRcvdBytes = 0;

uint16_t sentbytes = 0;

uint16_t _HT_Count = 0;
uint16_t _TC_Count = 0;
uint16_t _IDLE_Count = 0;

위에서 RxBuffer는 UART DMA RX 버퍼이고, wrPtr, rdPtr은 RxBuffer내의 valid한 데이터의 끝과 시작 위치를 가리킨다.

RxBuf3은 UART3이 수신한 데이터를 저장하는 사용자 버퍼이다.

다음은 UART IDLE Interrupt를 Enable하고 UART DMA RX를 개시하는 코드이다.

  __HAL_UART_ENABLE_IT(&huart3, UART_IT_IDLE);
  HAL_UART_Receive_DMA(&huart3, RxBuffer, MAX_RX_BUF + 16);

다음으로 UART DMA RX의 Interrupt 처리 루틴을 추가한다. HT Interrupt 처리를 위한 HAL_UART_RxHalfCpltCallback(), TC Interrupt 처리를 위한 HAL_UART_RxCpltCallback(), UART IDLE Interrupt 처리를 위한 UART_IDLECallback() 함수를 다음과 같이 구성한다.

main.c에 HAL_UART_RxHalfCpltCallback() 함수를 만들어 둔다.

... main.c ...

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	_TC_Count++;
	rcvFlag = 1;
}

void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
	_HT_Count++;
	rcvFlag = 1;
}


void UART_IDLECallback(UART_HandleTypeDef *huart)
{
	_IDLE_Count++;
	rcvFlag = 1;
}

위 두 개 함수는 weak 함수로 HAL 라이브러리에서 선언되어 있어서 따로 지정할 필요가 없지만 IDLE Interrupt 처리를 위한 UART_IDLECallback()은 함수명을 임의로 지정해도 된다.

위 세개의 인터럽트 처리를 위한 Callback함수에서 인터럽트가 발생했다는 Flag(rcvFlag)를 설정하면, while() 루프내에서 UART_Data_Process를 호출하는데, 이 함수는 UART DMA RX 버퍼내의 수신 데이터를 사용자 버퍼로 복사하는 기능을 한다.

... main.c ...

void UART_Data_Process(UART_HandleTypeDef *huart)
{
	wrPtr = ARRAY_LEN(RxBuffer) - huart->hdmarx->Instance->CNDTR;
	if(wrPtr != rdPtr)
	{
		memset(RxBuf3, 0, MAX_RX_BUF + 16);

		if (wrPtr > rdPtr)
		{
			rcvdLen = wrPtr - rdPtr;
			memcpy(RxBuf3, RxBuffer + rdPtr, rcvdLen);
		}else
		{
			rcvdLen = ARRAY_LEN(RxBuffer) - rdPtr;
			memcpy(RxBuf3, RxBuffer + rdPtr, rcvdLen);
			if(wrPtr > 0)
			{
				rcvdLen += wrPtr;
				memcpy(RxBuf3 + rcvdLen, RxBuffer, wrPtr);
			}
		}
		rdPtr = wrPtr;
	}
}

다음은 UART IDLE Interrupt 발생시에 인터럽트 서비스 루틴에서 IDLE 인터럽트를 클리어하고 위에 있는 UART_IDLECallback 이 수행되도록 처리한다.

...stmref1xx_it.c...

/**
  * @brief This function handles USART3 global interrupt.
  */
void USART3_IRQHandler(void)
{
  /* USER CODE BEGIN USART3_IRQn 0 */
  if(__HAL_UART_GET_FLAG(&huart3, UART_FLAG_IDLE) != RESET)
  {
	  __HAL_UART_CLEAR_IDLEFLAG(&huart3);
	  UART_IDLECallback(&huart3);
  }
  /* USER CODE END USART3_IRQn 0 */
  HAL_UART_IRQHandler(&huart3);
  /* USER CODE BEGIN USART3_IRQn 1 */

  /* USER CODE END USART3_IRQn 1 */
}

다음은 main.c내의 while() 루프에서 1초마다 UART4 포트로 DMA TX 전송을 하는 것과 수신이 발생했는지를 체크하는 코드를 추가한다.

... main.c ...
  
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
	  if(onesecondElapsed)
	  {
		  onesecondElapsed = 0;
		  count++;	// increment count
		  HAL_GPIO_TogglePin(DEBUG_LED_GPIO_Port, DEBUG_LED_Pin);
		  memset(dma_buf, 0, MAX_RX_BUF + 16);
		  printf("message: [%d] bytes, %s\r\n", strlen(message), message);
		  sprintf((char *)dma_buf, "UART4: %s\r\n", message);
		  printf("dma_buf: %s\r\n", dma_buf);
		  sentbytes = strlen((const char*)dma_buf);
		  HAL_UART_Transmit_DMA(&huart4, dma_buf, sentbytes);
		  printf("UART4 HAL_UART_Transmit_DMA sent: %d bytes\r\n", sentbytes);
		  totalSentBytes += sentbytes;
	  }
	  loopback_tcps(0, ethBuf0, 5000);
	  if(rcvFlag)
	  {
		  rcvFlag = 0;
		  UART_Data_Process(&huart3);
		  totalRcvdBytes += rcvdLen;
		  printf("RxBuf3: [%d] bytes, %s\r\n", strlen(RxBuf3), RxBuf3);
		  printf("_HT_Count: %d, _TC_Count: %d, _IDLE_Count: %d, rdPtr: %d, wrPtr: %d, rcvdLen: %d, totalSentBytes: %d, totalRcvdBytes: %d\r\n",
				  _HT_Count, _TC_Count, _IDLE_Count, rdPtr, wrPtr, rcvdLen, totalSentBytes, totalRcvdBytes);
	  }
  }
  /* USER CODE END 3 */

Build and Download

여기까지 완료되었으면 Build를 하고 STM32CubeProgrammer로 WIZ14xSR에 다운로드 한다.

실행화면

아래는 실행화면이다.

화면을 자세히 보면 하나의 DMA TX에 대해서 세번의 수신 데이터 출력이 있는 것을 확인할 수 있다.

여기까지의 전체 프로젝트는 아래 링크에서 다운로드할 수 있다.

https://github.com/javakys/WIZ14xSR_Proj/releases/tag/Ver0.7

1 Comment

Leave a Comment