그라운드 임베디드 EN 유튜브 구독

노트 / 컴퓨터구조

DMA: CPU 없이 데이터를 옮기는 일꾼

메모리와 주변장치 사이의 복사를 하드웨어가 대신합니다. ADC 샘플링, UART 수신, SPI 디스플레이 전송이 빨라지는 원리.

GROUND EMBEDDED · 정리 노트컴퓨터구조 09DMA 1static uint8_t rx_buf[256]; 2// DMA를 순환(circular) 모드로 설정: 끝까지 차면 처음부터 다시… 3uint16_t rx_tail; 4void uart_poll(void) {// 메인…

UART로 1000바이트를 받으면 인터럽트가 1000번 걸립니다. DMA를 쓰면 "UART 데이터 레지스터에서 이 버퍼로 1000바이트를 옮겨라"라고 한 번 설정하고, 끝났을 때 인터럽트 한 번만 받습니다.

주변장치ADC, UARTDMA옮기는 일꾼메모리 버퍼다 차면 CPU에 알림
DMA가 있으면 데이터가 CPU를 거치지 않고 바로 메모리로 갑니다.

설정하는 값

  • 출발지와 목적지 주소: 예) &USART1->DR → rx_buf
  • 전송 개수: 1000
  • 증가 여부: 주변장치 주소는 고정, 메모리 주소는 1씩 증가
  • 데이터 크기: 바이트, 하프워드, 워드
  • 트리거: 어떤 주변장치 이벤트마다 한 번씩 옮길지
  • 모드: 한 번(normal) 또는 끝나면 처음으로 돌아가 반복(circular)

자주 쓰는 조합

조합 효과
타이머 → ADC → DMA 정확한 주기로 샘플을 버퍼에 쌓음
UART 수신 + 순환 DMA + IDLE 인터럽트 길이를 모르는 패킷 수신
메모리 → SPI 디스플레이 화면 전송

주의할 점

  • DMA가 쓰는 중인 버퍼를 CPU가 읽으면 반쯤 바뀐 데이터를 봅니다. 절반 완료 인터럽트로 버퍼를 둘로 나눠 쓰는 더블 버퍼링을 씁니다.
  • 캐시가 있는 코어(M7)에서는 캐시 정리와 무효화가 필요합니다.

CPU가 하던 복사를 대신한다

UART로 1000바이트를 받을 때 인터럽트 방식은 핸들러가 1000번 불립니다. DMA를 쓰면 바이트가 도착할 때마다 하드웨어가 직접 버퍼로 옮기고, CPU는 다 끝났을 때 한 번만 알림을 받습니다.

폴링 인터럽트 DMA
CPU 사용 계속 기다림 바이트마다 끼어듦 끝날 때 한 번
빠른 데이터 놓치기 쉬움 핸들러가 밀리면 놓침 잘 견딤
설정 가장 쉬움 중간 가장 복잡
알맞은 곳 느리고 드문 일 바이트 단위 처리 긴 버퍼, 높은 속도

설정에 필요한 다섯 가지

  1. 출발지: 주변장치의 데이터 레지스터 주소 (예: &USART1->RDR)
  2. 도착지: RAM의 버퍼 주소
  3. 개수: 옮길 데이터 수
  4. 방향과 크기: 주변장치 → 메모리인지 반대인지, 한 번에 몇 비트인지
  5. 주소 증가: 버퍼 쪽은 증가, 주변장치 쪽은 고정

주변장치 쪽 주소를 증가로 두는 실수가 흔합니다. 데이터 레지스터는 항상 같은 주소이므로 고정이어야 합니다.

무료 전자책이 주제를 한 권으로 이어 읽기 (PDF 3권)→

순환 모드로 UART 수신하기

static uint8_t rx_buf[256];
// DMA를 순환(circular) 모드로 설정: 끝까지 차면 처음부터 다시 채움

uint16_t rx_tail;
void uart_poll(void) {                        // 메인 루프나 1 ms 틱에서
    uint16_t head = 256u - dma_remaining();   // DMA가 지금까지 쓴 위치
    while (rx_tail != head) {
        process_byte(rx_buf[rx_tail]);
        rx_tail = (rx_tail + 1u) & 255u;
    }
}

DMA가 쓰는 위치가 링 버퍼의 head 역할을 합니다. 인터럽트가 전혀 없어도 바이트를 놓치지 않습니다. 길이가 일정하지 않은 패킷은 UART의 IDLE 인터럽트(선이 한 글자 시간만큼 조용함)와 함께 쓰면 패킷의 끝을 알 수 있습니다.

ADC 여러 채널을 한꺼번에

ADC를 스캔 모드로 두고 DMA를 연결하면 채널 순서대로 배열에 값이 채워집니다.

volatile uint16_t adc_val[4];   // [0]=전류 A, [1]=전류 B, [2]=전압, [3]=온도

타이머가 ADC를 트리거하고 DMA가 결과를 옮기면, 코드는 배열을 읽기만 하면 됩니다.

절반 완료 인터럽트로 끊김 없이 처리

오디오처럼 계속 흐르는 데이터는 버퍼를 둘로 나눠 씁니다. 앞 절반이 찼다는 알림에서 앞쪽을 처리하고, 끝까지 찼다는 알림에서 뒤쪽을 처리합니다. DMA가 쓰고 있는 절반과 코드가 읽는 절반이 겹치지 않습니다.

현업에서 자주 터지는 문제

  • 버퍼가 지역 변수: 함수가 끝나면 스택이 재사용되는데 DMA는 계속 그 주소에 씁니다. 버퍼는 전역이나 static으로 둡니다.
  • 캐시가 있는 MCU(Cortex-M7): DMA가 바꾼 메모리와 캐시 내용이 달라집니다. 버퍼를 캐시 줄 단위로 정렬하고 무효화하거나 캐시하지 않는 영역에 둡니다.
  • 채널 충돌: 주변장치마다 쓸 수 있는 DMA 채널이 정해져 있어 둘이 같은 채널을 원할 수 있습니다. 설계 초기에 표로 확인합니다.
  • 전송 중에 설정 변경: 반드시 채널을 끄고 바꾼 뒤 다시 켭니다.
  • 오류 플래그 미처리: 전송 오류가 나면 채널이 멈춥니다. 오류 인터럽트에서 다시 시작하도록 합니다.
영상으로 이어 보기회로와 하드웨어 시리즈 8편전자제품 속 회로 소자는 어떻게 동작할까? · 저항 · 커패시터 · 인덕터 완벽 정리FREE PDF무료 전자책 3권 받기이 주제를 한 권으로 이어서 읽고 싶다면. BLDC 모터 제어, 직렬통신, 센서를 책 분량으로 묶었습니다. 이메일만 남기면 바로 받습니다.무료로 받기 →