임베디드를 좋아하는 조금 특이한 개발자?

[STM32] USART 데이터 손실 및 멈춤 문제 분석(Overrun 문제) 본문

Embedded/STM32

[STM32] USART 데이터 손실 및 멈춤 문제 분석(Overrun 문제)

Gordon_ 2026. 9. 28. 23:38
반응형

- 개발 환경

개발 보드 : NUCLEO-F103RB

개발 프로그램

- STM32 CubeMX : 6.18.1

- STM32CubeIDE for Visual Studio Code : 3.10.0


1. 서론

  STM32의 HAL 라이브러리를 사용하여 개발하는 경우 USART의 관련 함수를 사용하면서 많은 불편을 느끼는 경우가 많습니다. 예를 들어 가변 데이터를 입력 받기 힘든점이 있을 수 있습니다. 하지만 가장 큰 문제는 데이터의 손실 및 Overrun 에러가 발생하여 USART가 동작하지 않는 문제입니다. 이러한 문제가 발생하는 이유와 어떻게 문제를 해결할 수 있는지에 대해서 알아보도록 하겠습니다.

 

주의)

  여기서 소개하는 방법은 근본적인 Overrun 문제를 해결하는 방법이 아닙니다. 여기서 소개하는 방법은 Overrun 문제가 발생하는 원인을 확인하고 해당 문제를 해결하는 가장 간단한 방법을 소개합니다. Overrun 문제를 해결하기 위해서는 DMA를 사용한 Multibuffer 방식을 사용해야합니다.

https://littlebitodd-developer.tistory.com/130

 

[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_echoContribute to MainForm

littlebitodd-developer.tistory.com

 

2. Overrun 발생 이유

RM0008 Reference manual 795p

  RM0008 문서에 Overrun 에러는 "데이터를 수신하였을 때, RXNE레지스터가 reset 되지 않았다면 발생"한다고 적혀있습니다. 이 말을 해석하면 "이전 데이터를 읽지 않은 상태에서 새로운 데이터가 읽히면 발생"하는 것입니다.

 

2.1. RXNE Register 란?

 그럼 RXNE 레지스터에 대해서 자세히 알아보도록 하겠습니다. 

 

  RXNE 레지스터는 새로운 데이터가 도착했을때, 자동적으로 1로 Set이 되어 새로운 데이터가 읽을 수 있는 상태가 되었음을 알려주는 레지스터입니다. 

  그리고 USART_DR 레지스터에서 데이터를 읽을 때 마다, 자동적으로 RXNE가 0으로 reset되어 다음 데이터를 읽을 준비가 되도록 합니다. 그렇지 안다면 Overrun 에러가 발생한다는 것을 확인 할 수 있습니다.

 

3. Overrun 발생 확인

#include "main.h"
#include "gpio.h"
#include "usart.h"

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);

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();

    uint8_t data = 0;

    while (1){
        // 1 byte 데이터를 정상적으로 수신 했다면
        if(HAL_UART_Receive(&huart2, &data, 1, HAL_MAX_DELAY) == HAL_OK){
            // 수신된 1 byte를 다시 송신
            HAL_UART_Transmit(&huart2, &data, 1, HAL_MAX_DELAY);
        }
    }
}

 

  확인해보면 HAL 라이브러리를 사용해 1 byte씩 수신 받아 1 byte 씩 송신하는 간단한 예제입니다. 하지만 "0123456789_" 를 테스트로 계속 송신하다보면 아래 처럼 데이터가 랜덤하게 손실 되고 결국 Overrun이 발생하여 USART가 멈추는 일이 발생하게 됩니다.

 

 

3.1. HAL_UART_Transmit 함수 내부 데이터 송신 흐름

    Overrun이 발생하는 가장 큰 이유는 HAL_UART_Transmit가 어떻게 데이터를 송신하는지를 확인해보면 알 수 있습니다. 여기서 가장 중요한 것은 TC Flag를 확인하는 과정에서 Overrun 애러가 발생하는 경우가 많습니다.

 

위 흐름에서 TC Flag가 1일 때까지 대기하는 과정이 있습니다. 

https://github.com/MainForm/F103RB_UART_dma_echo/blob/main/Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_uart.c#L1194

    if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TC, RESET, tickstart, Timeout) != HAL_OK)
    {
      huart->gState = HAL_UART_STATE_READY;

      return HAL_TIMEOUT;
    }

 

 

 

  위 그림을 보면 알 수 있듯이 TC Flag가 사용되는 목적은 모든 데이터가 TX Line으로 전송이 완료되었음을 알려주는 Flag입니다. 그렇다는 것은 모든 데이터가 송신될 때 까지 바쁜 대기(Busy Waiting)를 한다는 것입니다. 그리고 바쁜 대기를 하는 도중에 새로운 데이터가 송신된다면 그 때 Overrun 애러가 발생하는 것입니다.

 

4. Overrun 발생 하는 데이터 처리 흐름

 

 위 타이밍 그래프는 Overrun이 발생하는 이유를 설명하기 위해 각 Flag가 어떻게 동작하는지를 시간축으로 그린 그래프입니다. 꽤 복잡하므로 각 구간 마다 설명하도록 하겠습니다.

 

4.1. RX Line 으로부터 데이터 수신

 

  여기서 중요한 것은 RXNE Flag가 어떻게 변화 하는지가 중요합니다. 먼저 RXNE Flag는 RX Line으로 새로운 데이터가 도착한 경우 1로 Set이 됩니다. 그리고 USART_RDR에 있는 데이터를 읽으면 자동적으로 0으로 Reset이 됩니다.

 

  그렇다면 RXNE Flag가 어디서 reset이 되는지 알아보도록 하겠습니다. 

 

https://github.com/MainForm/F103RB_UART_dma_echo/blob/main/Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_uart.c#L1282

        if ((huart->Init.WordLength == UART_WORDLENGTH_9B) || ((huart->Init.WordLength == UART_WORDLENGTH_8B) && (huart->Init.Parity == UART_PARITY_NONE)))
        {
          *pdata8bits = (uint8_t)(huart->Instance->DR & (uint8_t)0x00FF);
        }
        else
        {
          *pdata8bits = (uint8_t)(huart->Instance->DR & (uint8_t)0x007F);
        }

 

 위 코드는 HAL_UART_Receive 함수 내 일부 코드입니다. 해당 코드는 DR 레지스터로부터 데이터를 읽어오는 동작을 하고 있습니다. 그리고 DR 레지스터로 부터 데이터를 읽는 것이 RDR 레지스터에서 데이터를 읽는 것과 같으며 해당 코드고동작하면 자동적으로 RX가 0으로 Reset이 됩니다.

 

4.2. 수신 받은 데이터를 TX Line 로 데이터 수신

 여기서 가장 중요한 것은 5번 입니다. HAL_UART_Transmit 함수는 마지막 데이터의 Stop bit까지 전송을 완료할 때까지 바쁜 대기를 하기 때문에, 결국 HAL_UART_Receive 함수를 호출하는 시간이 밀려 결국 Overrun 에러가 발생할 수 밖에 없는 것입니다.

 

4.3. Fram2와 Fram3의 데이터 수신

 

 결국 HAL_UART_Transmit에서 딜레이가 발생하는 바람에 RXNE Flag가 1로 Set이 되는 기간이 길어지더니 Frame 4데이터가 도착하였을때 RXNE Flag가 1로 Set이 된 상태가 되어 결국 Overrun 에러가 발생하게 됩니다.

 

5. TC Flag를 바쁜 대기 하지 않는 코드로 변경


주의)

  여기서 소개하는 방법은 근본적인 Overrun 문제를 해결하는 방법이 아닙니다. 여기서 소개하는 방법은 Overrun 문제가 발생하는 원인을 확인하고 해당 문제를 해결하는 가장 간단한 방법을 소개합니다. Overrun 문제를 해결하기 위해서는 DMA를 사용한 Multibuffer 방식을 사용해야합니다.


#include "main.h"
#include "gpio.h"
#include "usart.h"

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);

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();

    uint8_t data = 0;

    while (1){
        // 1 byte 데이터를 정상적으로 수신 했다면 RXNE 플래그가 set됨
        if(huart2.Instance->SR & USART_SR_RXNE){
            // 수신된 데이터를 data에 저장
            // DR 레지스터를 읽으므로, RXNE 플래그가 자동으로 reset됨
            data = (uint8_t)(huart2.Instance->DR & 0xFF);

            // 데이터를 송신하기 전에 TDR(Transmit Data Register)가 비어있는지 확인
            while (!(huart2.Instance->SR & USART_SR_TXE));

            // 송신할 데이터를 TDR에 기록, DR 레지스터를 쓰므로 TXE 플래그가 자동으로 reset됨
            huart2.Instance->DR = data;
        }
    }

}

 

 HAL 라이브러리를 사용하지 않고 위 코드처럼 변경한다면 Overrun 문제를 해결 할 수 있습니다.

5.1. TC Flag를 바쁜 대기 하지 않을 때의 데이터 처리 흐름

  

TC Flag를 바쁜 대기를 하지 않으므로 바로 데이터를 수신하기 위한 준비가되어 연속적으로 데이터 처리가 원활해지는 것을 확인할 수 있습니다.

 

 

6. 결론

  STM32는 USART를 다루기가 매우 복잡하며 HAL 라이브러리 조차 이러한 문제를 가지고 있기 때문에 Nucleo 개발 보드가 아두이노처럼 유명해지지 않았다고 생각합니다. 만약 이러한 문제를 잘 해결하였다면 아두이노처럼 유명한 개발보드가 되어 있을 것으로 생각됩니다.

 

 그리고 지금 TC Flag로 바쁜 대기를 하지 않는 방법을 통해 간단히 Overrun 문제를 해결하였습니다. 하지만 이건 근본적인 해결방법이 아닙니다. 115200bps의 Bandrate를 사용한다면 1 byte를 86.8 us 마다 수신 해야하는 문제가 있습니다. 그러므로 이러한 문제를 해결하기 위해서는 DMA의 Multibuffer 방식을 사용해야합니다. 

 

https://littlebitodd-developer.tistory.com/130

 

[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_echoContribute to MainForm

littlebitodd-developer.tistory.com

 

반응형