DMA: CPU 없이 데이터를 옮기는 일꾼
메모리와 주변장치 사이의 복사를 하드웨어가 대신합니다. ADC 샘플링, UART 수신, SPI 디스플레이 전송이 빨라지는 원리.
UART로 1000바이트를 받으면 인터럽트가 1000번 걸립니다. DMA를 쓰면 "UART 데이터 레지스터에서 이 버퍼로 1000바이트를 옮겨라"라고 한 번 설정하고, 끝났을 때 인터럽트 한 번만 받습니다.
설정하는 값
- 출발지와 목적지 주소: 예)
&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 사용 | 계속 기다림 | 바이트마다 끼어듦 | 끝날 때 한 번 |
| 빠른 데이터 | 놓치기 쉬움 | 핸들러가 밀리면 놓침 | 잘 견딤 |
| 설정 | 가장 쉬움 | 중간 | 가장 복잡 |
| 알맞은 곳 | 느리고 드문 일 | 바이트 단위 처리 | 긴 버퍼, 높은 속도 |
설정에 필요한 다섯 가지
- 출발지: 주변장치의 데이터 레지스터 주소 (예:
&USART1->RDR) - 도착지: RAM의 버퍼 주소
- 개수: 옮길 데이터 수
- 방향과 크기: 주변장치 → 메모리인지 반대인지, 한 번에 몇 비트인지
- 주소 증가: 버퍼 쪽은 증가, 주변장치 쪽은 고정
주변장치 쪽 주소를 증가로 두는 실수가 흔합니다. 데이터 레지스터는 항상 같은 주소이므로 고정이어야 합니다.
무료 전자책이 주제를 한 권으로 이어 읽기 (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 모터 제어, 직렬통신, 센서를 책 분량으로 묶었습니다. 이메일만 남기면 바로 받습니다.무료로 받기 →