STM32에서 UART를 사용하면서 Overrun을 크게 신경 쓰지 않고 있었다. CLI 입력은 사용자가 키보드로 한 글자씩 입력하는 용도였고, 사람이 입력하는 속도라면 MCU가 충분히 따라갈 수 있다고 봤기 때문이다. 실제로 평소 사용에서는 별문제가 없었다. 문제는 시리얼 터미널에서 문자열을 Copy & Paste 했을 때 나타났다. 붙여넣은 문자열 중간에서 몇 글자가 빠졌고, 확인해보니 UART Overrun이 발생하고 있었다.
Copy & Paste와 CLI Echo
사람이 직접 키보드를 누르는 것과 Copy & Paste는 UART 입장에서 전혀 다른 입력이다. 직접 입력할 때는 문자 사이에 충분한 간격이 있지만, Paste를 하면 여러 문자가 짧은 시간에 연속으로 들어온다. 여기에 CLI 포트는 수신한 문자를 다시 시리얼 화면으로 출력하는 Echo 동작까지 있다. 평소에는 문제가 되지 않던 수신 처리 지연이 연속 입력과 Echo가 겹치는 순간 드러난 것으로 봤다.
기존 설정을 확인해보니 STM32G4의 UART FIFO는 사용하지 않고 있었다. CubeMX가 RX/TX FIFO threshold 설정 코드는 생성해 놓았지만, 마지막에는 FIFO mode를 Disable하고 있었다.
if (HAL_UARTEx_SetRxFifoThreshold(&hlpuart1, UART_RXFIFO_THRESHOLD_1_8) != HAL_OK) {
Error_Handler();
}
if (HAL_UARTEx_DisableFifoMode(&hlpuart1) != HAL_OK) {
Error_Handler();
}
8-entry RX FIFO 활성화
STM32G4 USART의 RX/TX FIFO depth는 8-entry다. 일반적인 8-bit UART라면 RX 쪽에서 최대 8개의 문자를 하드웨어가 임시로 받아둘 수 있다. 이번 수정은 단순했다. 사용 중인 UART/LPUART 포트의 FIFO mode를 모두 Enable로 변경했다.
- HAL_UARTEx_DisableFifoMode(&hlpuart1)
+ HAL_UARTEx_EnableFifoMode(&hlpuart1)
- HAL_UARTEx_DisableFifoMode(&huart4)
+ HAL_UARTEx_EnableFifoMode(&huart4)
RX threshold는 기존의 1/8 설정을 그대로 유지했다. STM32G4의 8-entry FIFO에서 1/8은 사실상 1개 데이터가 들어왔을 때 조건이 만족되는 설정이고, 수신도 계속 HAL_UART_Receive_IT(..., 1) 방식으로 처리한다. 따라서 이번 변경의 목적은 인터럽트 횟수를 줄이는 최적화가 아니다. 기존의 1-byte 수신 방식을 유지하면서, CPU가 순간적으로 UART를 처리하지 못하는 동안 뒤이어 들어오는 데이터를 FIFO가 받아주는 여유를 확보하는 데 목적이 있다.
FIFO와 오류 복구는 별개의 문제
FIFO를 켠 뒤 Codex로 현재 UART 코드를 다시 검토했다. 여기서 한 가지 지적이 나왔다. HAL은 ORE가 발생하면 오류 처리 과정에서 수신을 종료할 수 있는데, 기존 Error Callback은 로그만 남기고 있었고, 별도의 주기적 복구 함수는 UART ISR 레지스터의 오류 비트를 확인한 뒤 수신을 다시 시작하는 구조라는 점이었다.
다만 SECC 통신을 담당하는 GQSE task에서는 메시지 큐가 100ms 동안 비었을 때 UART 상태를 확인하고 있다.
gqse_timeout:
if (uart_check_isr_status(UartPort_SECC)) {
handle->rx.frame_discard_request = 1;
warn(" ---> SECC UART RECOVERY");
}
UART가 오류 상태로 남아 있으면 복구 함수에서 Abort 후 RX FIFO를 비우고 오류 플래그를 정리한 뒤 수신 인터럽트를 다시 시작한다.
HAL_UART_Abort(handle);
SET_BIT(handle->Instance->RQR, USART_RQR_RXFRQ);
handle->Instance->ICR =
USART_ICR_PECF |
USART_ICR_FECF |
USART_ICR_NECF |
USART_ICR_ORECF |
USART_ICR_RTOCF;
CLEAR_BIT(handle->Instance->CR3, USART_CR3_OVRDIS);
if (uart_cb[port] != NULL) {
HAL_UART_Receive_IT(handle, &data[port], 1);
}
그리고 UART만 정상화하는 것으로 끝내지 않고 frame_discard_request를 설정한다. UART 오류가 발생한 시점에 조립 중이던 프레임은 신뢰할 수 없으므로, 상위 수신 로직에서도 해당 프레임을 폐기하도록 한 것이다.
전체 UART FIFO 적용
이번 Overrun은 CLI 포트에서 먼저 드러났다. CLI는 Copy & Paste로 짧은 시간에 데이터가 몰릴 수 있고 Echo 동작도 있기 때문에, Echo가 없는 기존 모듈 간 UART 통신보다 Overrun이 발생할 가능성이 높다. ESP32나 SECC 같은 모듈 간 통신에서는 기존에도 같은 문제가 거의 나타나지 않았다. 그렇다고 CLI에만 FIFO를 적용할 이유는 없다고 판단했다. MCU가 이미 제공하는 하드웨어 FIFO를 그대로 사용할 수 있고, 현재 프로토콜이나 1-byte interrupt 구조를 바꾸지 않으면서도 순간적인 interrupt latency에 대한 수신 여유를 확보할 수 있기 때문이다. 그래서 이번 기회에 사용 중인 모든 UART/LPUART 포트에 FIFO를 활성화했다.
정리
이번 변경의 시작은 시리얼 터미널에서 문자열을 Copy & Paste 했을 때 몇 글자가 빠지는 작은 문제였다. 평소 사람이 직접 입력할 때는 충분히 느렸기 때문에 드러나지 않아, 애써 무시하고 있었지만 Paste로 연속 입력이 들어오고 CLI Echo까지 겹치면서 FIFO를 사용하지 않던 UART 수신 구조의 약점이 보였다.
현재는 RX threshold 1/8과 1-byte interrupt 수신 방식을 유지한 채 STM32G4의 8-entry FIFO를 활성화했다. 인터럽트를 묶어서 처리하기 위한 변경은 아니고, 순간적인 burst와 ISR latency에 대한 하드웨어 완충 공간을 확보하기 위한 변경이다. 그리고 FIFO와 별개로 UART가 오류 상태에 빠졌을 때는 Abort, FIFO Flush, 오류 플래그 정리, RX 재시작과 함께 현재 프로토콜 프레임도 폐기하는 기존 복구 흐름을 유지한다.
CLI에서 발견한 문제였지만, 이참에 전체 UART 포트에 같은 FIFO 설정을 적용하는 것으로 정리했다.