| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 디버깅
- USART
- yocto project
- AVR
- bare metal
- C++
- 아두이노
- Debug
- STM32
- Visual Studio Code
- c#
- raspberrypi
- buildroot
- 라즈베리파이
- atmel
- Arduino
- QEMU
- GPIO
- 리눅스
- WPF
- nucleo
- Raspberry
- yocto
- UART
- vscode
- QT
- robot
- Visual Studio
- 문자 장치
- Linux
- Today
- Total
임베디드를 좋아하는 조금 특이한 개발자?
[STM32] DMA를 통해 USART Overrun 문제 해결 본문
- 개발 환경
개발 보드 : NUCLEO-F103RB
개발 프로그램
- STM32 CubeMX : 6.18.1
- STM32CubeIDE for Visual Studio Code : 3.10.0
- 소스 코드
https://github.com/MainForm/F103RB_UART_dma_echo
GitHub - MainForm/F103RB_UART_dma_echo
Contribute to MainForm/F103RB_UART_dma_echo development by creating an account on GitHub.
github.com
위 소스 코드는 아래 포스트 기반으로 작성되었습니다.
https://littlebitodd-developer.tistory.com/127
[STM32] STM32 CubeMX으로 C++ 개발 환경 구축
- 개발 환경개발 보드 : NUCLEO-F103RB개발 프로그램- STM32 CubeMX : 6.18.1- STM32CubeIDE for Visual Studio Code : 3.10.0- 소스 코드https://github.com/MainForm/STM32_cpp_template GitHub - MainForm/STM32_cpp_templateContribute to MainForm/STM
littlebitodd-developer.tistory.com
1. 서론
UART는 다양한 MCU에서 개발할 때 logging 용도로 많이 사용하는 통신이므로 많이 사용합니다. 그리고 해당 통신이 정상적으로 동작하는지 확인하기 위해 echo를 구현하는 것이 일반적입니다. 그리고 실제로 아두이노나 ESP32 개발 보드에서는 echo를 구현하는 것이 매우 쉽고 직관적입니다. 하지만, STM32에서는 그렇지 않습니다. STM32의 UART는 내부 버퍼(RDR)도 1 byte 밖에 없어서 Overrun이 발생하기 쉬운 환경에 있습니다. 그러므로 STM32에서 Multibuffer로 구현하는 방법으로 DMA를 사용하는 것을 소개하고 있습니다. 그래서 이번 포스트에서는 USART에 DMA를 적용하여 Multibuffer로 Overrun이 발생하지 않는 개발 방법을 소개해드립니다.
만약 Overrun이 어떻게 발생하는지 알고 싶은 분은 아래 포스트를 확인해주시기 바랍니다.
https://littlebitodd-developer.tistory.com/129
[STM32] USART 데이터 손실 및 멈춤 문제 분석(Overrun 문제)
- 개발 환경개발 보드 : NUCLEO-F103RB개발 프로그램- STM32 CubeMX : 6.18.1- STM32CubeIDE for Visual Studio Code : 3.10.01. 서론 STM32의 HAL 라이브러리를 사용하여 개발하는 경우 USART의 관련 함수를 사용하면서 많은
littlebitodd-developer.tistory.com
2. USART에 DMA 적용


여기서 Memory의 Data Width를 byte로 하는 이유는 USART로 1 byte 씩 입력 받기 때문입니다.
2.1. Normal mode 와 Circular Mode

DMA를 설정할때 메모리에 USART로 입력받은 데이터를 순차적으로 저장합니다. 이떄, Normal mode의 경우 미리 할당한 데이터가 가득 찬다면 그 즉시 DMA가 종료됩니다. 그 경우 다시 DMA를 Request를 반복적으로 수행하여야 하는 단점이 있습니다.

Circular Mode의 경우 메모리가 원형으로 구성되어 DMA가 끊기지 않고 데이터를 저장하게 됩니다. 위 그림에서 보이듯이 데이터가 가득 차게 된다면 다시 0번으로 돌아와 데이터를 저장하는 순환구조를 가지고 있습니다. 그러므로 처음 DMA Request를 하면 반복적으로 Request를 할 필요가 없습니다.
3. USART Echo 구현
https://github.com/MainForm/F103RB_UART_dma_echo/blob/main/App/Src/main.cpp
#include "main.h"
#include "dma.h"
#include "gpio.h"
#include "usart.h"
#include <queue>
#include <string>
#define RX_BUFFER_SIZE 10
using namespace std;
// Defined in Core/Src/main.c
// extern "C" disables C++ name mangling so the C++ linker can find the C function.
extern "C"
void SystemClock_Config(void);
// UART 수신 버퍼
string rxBuffer(RX_BUFFER_SIZE,'\0');
// 데이터의 첫번째 인덱스
int curIdx = 0;
queue<string> rxQueue;
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size){
if(huart->Instance == USART2){
// rxBuffer에서 curIdx부터 Size만큼 데이터를 rxQueue에 넣음
rxQueue.push(rxBuffer.substr(curIdx, Size - curIdx));
// curIdx를 다음 시작 위치로 이동
curIdx = Size % rxBuffer.size();
}
}
int main(void){
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART2_UART_Init();
// Circular 모드로 동작하기 떄문에 한번만 호출하면 됨
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, (uint8_t*)rxBuffer.c_str(), rxBuffer.size());
// Half Transfer Interrupt 비활성화(필요 없는 인터럽트)
__HAL_DMA_DISABLE_IT(huart2.hdmarx, DMA_IT_HT);
while (1){
// rxQueue에 데이터가 있으면 출력
if(rxQueue.empty() == false){
// 큐에서 맨 앞의 문자열을 꺼내서 출력
string msg = rxQueue.front();
rxQueue.pop();
// 문자열 출력
HAL_UART_Transmit(&huart2, (uint8_t*)msg.c_str(), msg.size(), HAL_MAX_DELAY);
}
}
}
주의)
- STL 사용에 대하여
먼저 위 코드에서 몇가지 주의사항이 있습니다. 저는 Queue를 직접 구현하기 힘들고 지금 코드는 예제 코드이므로 쉽고 간단하게 기능이 동작하는 것을 보여주기 위해 C++ STL를 사용하여 구현하였습니다. 실제 제품을 만들거나 안정성이 높은 기능을 구현할 때에는 C++ STL를 사용하는 것은 피하여야합니다. 그 이유는 C++ STL 내부에 Heap 메모리를 사용하기에 특히 임베디드에서는 자원이 제한된 환경이므로 Heap 메모리 할당 및 해제는 민감한 부분입니다.
USART의 입력을 DMA를 사용하여 SRAM에 Multi-buffer로 받아 처리하는 구조입니다. 그리고 입력이 완료될 때마다 인터럽트를 발생하여 입력 받은 데이터를 Queue에 넣에 Main Loop으로 전달하는 구조입니다. 해당 흐름을 Flow Chart로 간략하게 그리면 다음과 같습니다.

3.1. USART DMA 초기 설정
#define RX_BUFFER_SIZE 10
using namespace std;
// UART 수신 버퍼
string rxBuffer(RX_BUFFER_SIZE,'\0');
int main(void){
//초기화 코드
// Circular 모드로 동작하기 떄문에 한번만 호출하면 됨
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, (uint8_t*)rxBuffer.c_str(), rxBuffer.size());
// Half Transfer Interrupt 비활성화(필요 없는 인터럽트)
__HAL_DMA_DISABLE_IT(huart2.hdmarx, DMA_IT_HT);
while (1){
// Loop 코드
}
}
USART의 DMA를 초기화 하는 코드는 "HAL_UARTEx_RecieveToIdle_DMA" 함수입니다. 해당 함수로 USART로 수신되는 데이터를 어떤 주소에 저장할지 초기화하는 함수입니다.
string rxBuffer(RX_BUFFER_SIZE,'\0');
저는 rxBuffer를 DMA의 Buffer로서 설정하였습니다. rxBuffer는 제가 string 클래스로 사용하였지만, C언어로 변경하면 다음과 같습니다.
# string rxBuffer(RX_BUFFER_SIZE,'\0'); 를 C언어로 변경
char rxBuffer[RX_BUFFER_SIZE] = "";
결국 길이가 10인 char 배열을 USART의 버퍼로 사용한다는 것입니다.
주의)
저는 예제를 위해서 버퍼의 길이를 10으로 하였지만, 실제로는 버퍼의 길이를 넉넉히 주어야합니다.
DMA를 초기화 하였다는 USART는 데이터를 다음과 같은 흐름으로 수신하게 됩니다.

3.1.1. USART DMA 의 인터럽트 종류
- HAL_UART_RXEVENT_HT (Half Transfer)
HT 인터럽트는 DMA로 설정한 Buffer가 절반(Half)까지 데이터가 저장되었을 때 호출 되는 인터럽트입니다.

- HAL_UART_RXEVENT_TC (Transfer Complete)
TC 인터럽트는 버퍼에 데이터가 끝까지 저장완료 하였을떄 발생하는 인터럽트입니다.

- HAL_UART_RXEVENT_IDLE
Idle 인터럽트는 데이터 수신후 1 Frame간 데이터의 수신이 없을때 발생하는 인터럽트입니다.

여기서 1 Frame이라는 것은 USART에서 송수신하는 최소 단위입니다. 그리고 USART에서 1 Frame을 송수신하기 위한 최소 시간은 Bandrate에 따라 달라집니다. 예를들어, 가장 보편적으로 사용하는 115200 Brandrate에 8 bits, 1 Stop bit, None parity 라고 한다면 1Frame을 보내기 위해서 10 bits(1 Start bit + 8 Data bits + 1 Stop bit)가 필요합니다. 그리고 115200 bps이므로 10 bit을 전송하기 위해 10/115200 = 86.8 us 입니다. 그러므로 USART에서 마지막으로 수신한 Frame 이후 86.8 us 이후 Idle 인터럽트가 발생하는 것입니다.
3.1.2. HT 인터럽트 비활성화 하는 이유
// Half Transfer Interrupt 비활성화(필요 없는 인터럽트)
__HAL_DMA_DISABLE_IT(huart2.hdmarx, DMA_IT_HT);
저의 경우 HT 인터럽트를 비활성화 하였습니다. 그 이유는 Idle 인터럽트가 발생하므로 굳이 HT 인터럽트가 필요 없는 것입니다. Idle 인터럽트는 HT와 중복으로 호출됩니다. 단, TC와는 중복으로 호출되지 않으므로, TC 인터럽트는 활성화하여야 합니다.
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size){
switch(HAL_UARTEx_GetRxEventType(huart)){
case HAL_UART_RXEVENT_IDLE:
HAL_UART_Transmit(huart,(const uint8_t*)"IDLE\n", 5, 1);
break;
case HAL_UART_RXEVENT_TC:
HAL_UART_Transmit(huart, (const uint8_t*)"TC\n", 3, 1);
break;
case HAL_UART_RXEVENT_HT:
HAL_UART_Transmit(huart, (const uint8_t*)"HT\n", 3, 1);
break;
}
if(huart->Instance == USART2){
// rxBuffer에서 curIdx부터 Size만큼 데이터를 rxQueue에 넣음
rxQueue.push(rxBuffer.substr(curIdx, Size - curIdx));
// curIdx를 다음 시작 위치로 이동
curIdx = Size % rxBuffer.size();
}
}
위 처럼 ISR내부에서 어떤 인터럽트가 발생했는지 확인할 수 있습니다. 이때 HT를 활성화 하고 테스트를 진행해보도록 하겠습니다.

보이는 것처럼 HT와 IDLE이 중복으로 호출되는 것을 확인할 수 있습니다.

다만, TC의 경우는 IDLE이 호출되지 않는 것을 확인할 수 있습니다. 그러므로 HC는 IDLE로 대체가 가능하므로 HT를 비활성화 하였습니다.
3.2. USART DMA ISR 핸들러 함수
// UART 수신 버퍼
string rxBuffer(RX_BUFFER_SIZE,'\0');
// 데이터의 첫번째 인덱스
int curIdx = 0;
queue<string> rxQueue;
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size){
if(huart->Instance == USART2){
// rxBuffer에서 curIdx부터 Size만큼 데이터를 rxQueue에 넣음
rxQueue.push(rxBuffer.substr(curIdx, Size - curIdx));
// curIdx를 다음 시작 위치로 이동
curIdx = Size % rxBuffer.size();
}
}

USART DMA ISR 핸들러 함수는 USART로부터 입력받은 데이터가 rxBuffer에 저장되어 있고, 해당 데이터를 rxQueue에 Push하는 역할을 합니다.
하지만 문제가 되는 부분은 Buffer의 크기를 넘어서는 데이터가 수신되었을 때입니다. 하지만 저희는 DMA를 Circular Mode로 사용하고 있기에 Buffer의 처음부터 다시 데이터가 자동적으로 저장되므로 큰 걱정할 필요는 없습니다.


위 상황은 Buffer 크기보다 긴 "abcdefghijkl"의 데이터가 수신되었을 때의 상황입니다. 먼저 rxBuffer의 나머지 공간에 데이터를 수신하고 TC 인터럽트를 통해 rxQueue에 데이터를 Push합니다. 그 후 나머지 데이터를 rxBuffer의 처음부터 저장하여 동일하게 처리합니다.
3.3. Main 함수 Loop
queue<string> rxQueue;
int main(void){
while (1){
// rxQueue에 데이터가 있으면 출력
if(rxQueue.empty() == false){
// 큐에서 맨 앞의 문자열을 꺼내서 출력
string msg = rxQueue.front();
rxQueue.pop();
// 문자열 출력
HAL_UART_Transmit(&huart2, (uint8_t*)msg.c_str(), msg.size(), HAL_MAX_DELAY);
}
}
}
위 코드는 따로 설명할 필요도 없이 rxQueue에 저장된 데이터를 순서대로 Pop하여 USART로 출력하는 코드입니다.
4. 결과

실제로 정상적으로 동작하는 것을 확인할 수 있습니다. 매우 긴 문자열도 손실된 데이터나 멈추는 문제가 발생하지 않습니다.
5. 결론
USART의 overrun 문제는 꽤나 치명적인 문제라고 생각합니다. 물론 능숙한 개발자에게는 큰 문제가 되지 않지만 Nucleo 개발보드가 입문용인 개발 보드인 만큼 그리고 초보자가 많이 사용하는 USART인 만큼 다루기 쉬어야 한다고 생각합니다. 하지만 정상 동작하기 위해서는 DMA까지 공부하기에 많은 공부량을 요구하며 실제로 제가 이번 포스트에서 DMA에 대해 더 자세히 다루지 않았던 부분도 있습니다. 그렇기에 매우 아쉬운 부분이지만 최신 Nucleo-C562RE에서는 DMA를 사용하지 않고도 FIFO를 지원하여 이러한 문제를 해결하였습니다. 그러므로 Nucleo 개발보드에 편의성이 증가하여 보다 많은 사람들이 쉽게 STM32 개발보드에 접근할 수 있었으면 좋겠습니다.
'Embedded > STM32' 카테고리의 다른 글
| [STM32] USART 데이터 손실 및 멈춤 문제 분석(Overrun 문제) (0) | 2026.09.28 |
|---|---|
| [STM32] git으로 협업하기 위한 FreeRTOS 기반 프로젝트 구조 (0) | 2026.09.25 |
| [STM32] STM32 CubeMX으로 C++ 개발 환경 구축 (0) | 2026.09.22 |
| [STM32] VisualStudio Code에서 개발 환경 구축 (0) | 2026.03.07 |
| [STM32] PWM를 통한 SG-90 서보 모터 제어 (1) | 2025.04.10 |
