본문 바로가기
MCU

임베디드 시스템 DMA & 링 버퍼: 인터랙티브 가이드

by linuxgo 2025. 8. 25.
임베디드 시스템: DMA와 링 버퍼를 활용한 효율적인 데이터 통신

임베디드 시스템: DMA & 링 버퍼

STM32를 예시로 한 인터랙티브 가이드

왜 이 주제가 중요한가요?

임베디드 시스템에서 DMA(Direct Memory Access)는 CPU의 부담을 줄여주는 핵심 기술입니다. **STM32와 같은 마이크로컨트롤러**에서 DMA만으로는 가변 길이 데이터나 예기치 않은 통신 중단 상황에서 데이터 유실이 발생할 수 있습니다. 이 가이드는 **소프트웨어 링 버퍼**를 결합하는 것이 왜 안정적인 시스템 설계를 위한 최선의 전략인지 시각적으로 보여줍니다.

한계점: 고정 크기 수신에만 의존

가장 단순한 DMA 수신 방식은 `HAL_UART_Receive_DMA()`와 같이 고정된 크기의 버퍼를 사용하는 것입니다. 이 방식은 버퍼가 지정된 크기만큼 정확히 채워졌을 때만 콜백 함수가 호출되어 데이터를 처리할 수 있습니다. 이는 가변 길이 패킷 처리나 데이터 유실 위험에 매우 취약합니다.

인터랙티브 시뮬레이션

아래 버튼을 눌러 다양한 크기의 데이터 패킷 전송을 시뮬레이션 해보세요.

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

결론: 안정성을 위한 최선의 전략

실용적인 통신 시스템에서는 DMA만으로 충분하지 않습니다. 가변 길이 데이터 수신이 필요한 경우, **소프트웨어 링 버퍼**를 결합하는 것이 데이터 유실을 방지하고 시스템의 안정성을 극대화하는 최선의 전략입니다.