임베디드 시스템: DMA & 링 버퍼
STM32를 예시로 한 인터랙티브 가이드
왜 이 주제가 중요한가요?
임베디드 시스템에서 DMA(Direct Memory Access)는 CPU의 부담을 줄여주는 핵심 기술입니다. **STM32와 같은 마이크로컨트롤러**에서 DMA만으로는 가변 길이 데이터나 예기치 않은 통신 중단 상황에서 데이터 유실이 발생할 수 있습니다. 이 가이드는 **소프트웨어 링 버퍼**를 결합하는 것이 왜 안정적인 시스템 설계를 위한 최선의 전략인지 시각적으로 보여줍니다.
한계점: 고정 크기 수신에만 의존
가장 단순한 DMA 수신 방식은 `HAL_UART_Receive_DMA()`와 같이 고정된 크기의 버퍼를 사용하는 것입니다. 이 방식은 버퍼가 지정된 크기만큼 정확히 채워졌을 때만 콜백 함수가 호출되어 데이터를 처리할 수 있습니다. 이는 가변 길이 패킷 처리나 데이터 유실 위험에 매우 취약합니다.
인터랙티브 시뮬레이션
아래 버튼을 눌러 다양한 크기의 데이터 패킷 전송을 시뮬레이션 해보세요.
해결책: DMA와 링 버퍼의 시너지
이 방식은 DMA 순환 모드와 **Idle Line Detection** 인터럽트를 결합합니다. DMA는 하드웨어 버퍼에 데이터를 지속적으로 채우고, CPU는 인터럽트 발생 시 이를 소프트웨어 링 버퍼로 안전하게 옮겨 처리하는 효율적인 분업 구조를 구축합니다. 이로써 데이터 유실을 방지하고 가변 길이 패킷을 완벽하게 처리할 수 있습니다.
인터랙티브 시뮬레이션
데이터가 DMA 버퍼로 수신된 후, 'Idle' 이벤트가 발생하면 소프트웨어 링 버퍼로 안전하게 복사됩니다.
STM32 DMA & 링 버퍼 전체 C 코드
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
// 이 코드는 실제 STM32 마이크로컨트롤러에서 실행되는 C 코드를
// 시뮬레이션하기 위한 예제입니다. 브라우저에서 직접 실행되지는 않습니다.
// =========================================================================
// 전역 변수 및 매크로 정의
// =========================================================================
// --- DMA (링 버퍼 없음) 방식 관련 ---
#define NO_RING_BUFFER_SIZE 32
uint8_t dma_rx_buffer_no_ring[NO_RING_BUFFER_SIZE];
bool no_ring_data_ready = false;
// --- DMA (링 버퍼 사용) 방식 관련 ---
#define RING_BUFFER_SIZE 128
uint8_t dma_rx_buffer_ring[RING_BUFFER_SIZE];
uint8_t rx_ring_buffer[RING_BUFFER_SIZE];
volatile uint32_t head = 0; // 쓰기 포인터
volatile uint32_t tail = 0; // 읽기 포인터
// =========================================================================
// STM32 HAL 라이브러리 함수 시뮬레이션
// =========================================================================
// UART 데이터 수신 콜백 함수 시뮬레이션 (링 버퍼 없음)
void HAL_UART_RxCpltCallback(uint16_t size) {
if (size >= NO_RING_BUFFER_SIZE) {
printf(" [콜백] HAL_UART_RxCpltCallback() 호출됨. 버퍼가 가득 찼습니다.\n");
no_ring_data_ready = true;
}
}
// UART 데이터 수신 이벤트 콜백 함수 시뮬레이션 (링 버퍼 사용)
void HAL_UARTEx_RxEventCallback(uint16_t size) {
printf(" [콜백] HAL_UARTEx_RxEventCallback() 호출됨. 수신된 데이터 크기: %d 바이트\n", size);
for (uint16_t i = 0; i < size; i++) {
rx_ring_buffer[head] = dma_rx_buffer_ring[i];
head = (head + 1) % RING_BUFFER_SIZE;
}
printf(" [링 버퍼] 데이터 복사 완료. 현재 링 버퍼 상태:\n");
for (uint32_t i = 0; i < (head - tail + RING_BUFFER_SIZE) % RING_BUFFER_SIZE; i++) {
printf("%02X ", rx_ring_buffer[(tail + i) % RING_BUFFER_SIZE]);
}
printf("\n");
}
// 링 버퍼에서 데이터 읽기 함수
uint8_t read_from_ring_buffer() {
if (head == tail) {
return 0;
}
uint8_t data = rx_ring_buffer[tail];
tail = (tail + 1) % RING_BUFFER_SIZE;
return data;
}
// =========================================================================
// 메인 프로그램 로직
// =========================================================================
// 시뮬레이션 함수: 링 버퍼가 없는 DMA 방식
void simulate_no_ring_buffer() {
printf("\n--- 시뮬레이션 시작: 링 버퍼 없는 DMA 방식 ---\n");
printf("\n[시나리오 1] 10바이트 데이터 전송...\n");
memset(dma_rx_buffer_no_ring, 0, NO_RING_BUFFER_SIZE);
no_ring_data_ready = false;
for (int i = 0; i < 10; i++) {
dma_rx_buffer_no_ring[i] = 'A' + i;
}
printf(" DMA 버퍼에 10바이트가 채워졌지만...\n");
HAL_UART_RxCpltCallback(10);
if (!no_ring_data_ready) {
printf(" >>> 결과: 버퍼가 가득 차지 않아 콜백이 호출되지 않았습니다. 데이터가 버퍼에 갇힘!\n");
}
printf("\n[시나리오 2] 32바이트 데이터 전송...\n");
memset(dma_rx_buffer_no_ring, 0, NO_RING_BUFFER_SIZE);
no_ring_data_ready = false;
for (int i = 0; i < 32; i++) {
dma_rx_buffer_no_ring[i] = 'A' + i;
}
printf(" DMA 버퍼에 32바이트가 채워졌습니다.\n");
HAL_UART_RxCpltCallback(32);
if (no_ring_data_ready) {
printf(" >>> 결과: 버퍼가 가득 차 콜백이 성공적으로 호출되었습니다.\n");
printf(" 수신된 데이터: %s\n", dma_rx_buffer_no_ring);
}
}
// 시뮬레이션 함수: 링 버퍼를 사용한 DMA 방식
void simulate_with_ring_buffer() {
printf("\n--- 시뮬레이션 시작: 링 버퍼 사용 DMA 방식 ---\n");
printf("\n[시나리오 1] 15바이트 데이터 전송...\n");
memset(dma_rx_buffer_ring, 0, RING_BUFFER_SIZE);
for (int i = 0; i < 15; i++) {
dma_rx_buffer_ring[i] = 'a' + i;
}
printf(" DMA가 15바이트 수신 중... (Idle line 감지)\n");
HAL_UARTEx_RxEventCallback(15);
printf("\n[시나리오 2] 30바이트 데이터 전송...\n");
memset(dma_rx_buffer_ring, 0, RING_BUFFER_SIZE);
for (int i = 0; i < 30; i++) {
dma_rx_buffer_ring[i] = 'A' + i;
}
printf(" DMA가 30바이트 수신 중... (Idle line 감지)\n");
HAL_UARTEx_RxEventCallback(30);
printf("\n[메인 루프] 링 버퍼의 데이터 처리 시작...\n");
printf(" 처리된 데이터: ");
while (head != tail) {
uint8_t data = read_from_ring_buffer();
printf("%c", data);
}
printf("\n");
printf(" >>> 결과: 링 버퍼의 모든 데이터가 안전하게 처리되었습니다!\n");
}
int main() {
simulate_no_ring_buffer();
simulate_with_ring_buffer();
return 0;
}