본문으로 건너뛰기
Embedded Performance Engineering · 21/57

DMA 성능 최적화 — Burst·Scatter-Gather·Chain·Cache 일관성

· Hawk · 5분 읽기

#한 줄 요약

“DMA는 CPU를 우회한 데이터 이동입니다.” burst size와 alignment, cache가 핵심을 결정합니다.

#DMA Throughput 결정 요소

요소영향
Burst size클수록 throughput이 올라갑니다 (overhead 분산)
Beat sizebus 폭에 맞춥니다. 32-bit bus는 32-bit beat가 적합합니다
Alignmentmisaligned면 split이 발생합니다
OutstandingDMA controller가 동시에 처리하는 transaction 수입니다
Bus contention다른 master의 영향을 받습니다

#Burst Size 영향

/* 매 transfer 1 byte */
HAL_DMA_Init(&hdma, .PeriphDataAlignment = DMA_PDATAALIGN_BYTE);
/* → AXI overhead per byte → 매우 느림 */
/* 4-beat burst, 4-byte beat */
HAL_DMA_Init(&hdma,
.PeriphDataAlignment = DMA_PDATAALIGN_WORD,
.BurstSize = DMA_BURST_INC4);
/* → 16-byte per transaction → 10x throughput */

Burst length × Beat size가 한 AXI transaction의 크기입니다. cache line 크기인 64-byte가 sweet spot입니다.

#STM32H7 MDMA - Master DMA

MDMA_HandleTypeDef hmdma;
hmdma.Init.Request = MDMA_REQUEST_SW;
hmdma.Init.TransferTriggerMode = MDMA_BUFFER_TRANSFER;
hmdma.Init.Priority = MDMA_PRIORITY_HIGH;
hmdma.Init.Endianness = MDMA_LITTLE_ENDIANNESS_PRESERVE;
hmdma.Init.SourceInc = MDMA_SRC_INC_WORD;
hmdma.Init.DestinationInc = MDMA_DEST_INC_WORD;
hmdma.Init.SourceDataSize = MDMA_SRC_DATASIZE_WORD;
hmdma.Init.DestDataSize = MDMA_DEST_DATASIZE_WORD;
hmdma.Init.DataAlignment = MDMA_DATAALIGN_PACKENABLE;
hmdma.Init.BufferTransferLength = 128; // burst length
HAL_MDMA_Start(&hmdma, src, dst, len, 1);

MDMA는 AXI master여서 DRAM ↔ DRAM copy까지 가능합니다. 일반 DMA1/2는 peripheral ↔ memory만 지원합니다.

#Scatter-Gather

Source: Destination:
[fragment 1 @0x1000] [block @0x80000000]
[fragment 2 @0x4500]
[fragment 3 @0x9100]
[fragment 4 @0xC200]

DMA descriptor list:

typedef struct {
uint32_t src;
uint32_t dst;
uint32_t len;
uint32_t next_descriptor; // null = 종료
} dma_desc_t;
dma_desc_t descriptors[4] = {
{ .src=0x1000, .dst=base+0, .len=256, .next_descriptor=&descriptors[1] },
{ .src=0x4500, .dst=base+256, .len=512, .next_descriptor=&descriptors[2] },
{ .src=0x9100, .dst=base+768, .len=128, .next_descriptor=&descriptors[3] },
{ .src=0xC200, .dst=base+896, .len=256, .next_descriptor=NULL },
};

CPU 개입이 전혀 없습니다. DMA controller가 전체 chain을 자동으로 처리합니다.

#Chain Transfer (Linked List)

Frame 1 capture: DMA → buf_a
complete IRQ
Frame 2 capture: DMA → buf_b
complete IRQ
...

Camera·display·audio에서 연속 스트림을 처리할 때 씁니다. CPU는 processing에만 집중합니다.

#Double Buffer

Buf A: filled by DMA | Buf B: processed by CPU
swap
Buf A: processed | Buf B: filled
/* STM32 DMA — Double Buffer mode */
HAL_DMAEx_MultiBufferStart(&hdma, src, dst_a, dst_b, len);
void HAL_DMA_M0CpltCallback(DMA_HandleTypeDef *hdma) {
/* dst_a 완료 — process_a() */
}
void HAL_DMA_M1CpltCallback(DMA_HandleTypeDef *hdma) {
/* dst_b 완료 — process_b() */
}

DMA와 CPU가 동시에 동작하면서 throughput이 약 2배로 늘어납니다.

#Cache 일관성 - Cortex-M7

#CPU가 buffer write → DMA가 send

fill_data(tx_buf, 256); // CPU writes (cache only)
SCB_CleanDCache_by_Addr(tx_buf, 256); // ← memory에 flush
HAL_UART_Transmit_DMA(&huart1, tx_buf, 256);

Clean은 cache 내용을 memory로 write-back합니다.

#DMA가 buffer fill → CPU가 read

HAL_UART_Receive_DMA(&huart1, rx_buf, 256);
/* DMA complete */
SCB_InvalidateDCache_by_Addr(rx_buf, 256); // ← cache 폐기
process(rx_buf);

Invalidate는 stale cache line을 폐기합니다. 이후 CPU read는 memory에서 fresh하게 읽어옵니다.

#Misalignment 함정

SCB_CleanDCache_by_Addr(buf, 100); // 100 byte

Cache line이 32 byte라고 가정합니다. buf32-byte aligned가 아니거나 lenline 배수가 아니면 이웃 line까지 영향을 받습니다.

__attribute__((aligned(32))) uint8_t buf[128]; // line aligned
SCB_CleanDCache_by_Addr(buf, 128); // 4 line, exact

#Non-Cacheable Memory 영역

/* MPU로 non-cacheable region 설정 */
MPU_Region_InitTypeDef region = {0};
region.BaseAddress = 0x24000000;
region.Size = MPU_REGION_SIZE_512KB;
region.AccessPermission = MPU_REGION_FULL_ACCESS;
region.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
region.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
HAL_MPU_ConfigRegion(&region);
/* 이 영역 변수는 cache 관리 불필요 */
__attribute__((section(".uncached"))) uint8_t dma_buf[4096];

DMA 전용 buffer는 non-cacheable로 두면 cache maintenance overhead가 사라집니다.

#DMA Latency

Setup overhead - 매 transfer 시작 비용:

  • STM32 DMA: ~5 cycle
  • i.MX SDMA: ~50 cycle (FW 실행)
  • Linux dmaengine: ~100 cycle + IRQ

작은 transfer (< 256 byte)에서는 DMA setup 비용이 더 큽니다. 이 구간은 CPU memcpy가 빠릅니다.

#CPU vs DMA - 손익 분기점

  • memcpy: 1 byte ≈ 0.5 cycle (Cortex-M4)
  • DMA setup 50 cycle + transfer cost ( 1 byte/cycle)

CPU cost=0.5N,DMA cost=50+1N\text{CPU cost} = 0.5 \cdot N, \quad \text{DMA cost} = 50 + 1 \cdot N

cross-over:50+N=0.5N    N=100 (?)\text{cross-over}: \quad 50 + N = 0.5 \cdot N \implies N = -100 \ (?)

실제로는 DMA가 CPU를 자유롭게 만들어 CPU offload가 진짜 이득입니다.

작은 copy는 CPU가 유리합니다. 큰 copy(>256 byte)는 DMA가 유리합니다. CPU가 다른 일을 할 수 있을 때는 DMA가 무조건 유리합니다.

#Linux dmaengine API

struct dma_chan *chan = dma_request_chan(dev, "rx");
struct dma_async_tx_descriptor *tx;
tx = dmaengine_prep_dma_memcpy(chan, dst, src, len, DMA_PREP_INTERRUPT);
tx->callback = dma_done_cb;
dma_cookie_t cookie = dmaengine_submit(tx);
dma_async_issue_pending(chan);
/* Wait */
dma_sync_wait(chan, cookie);

Linux에서는 peripheral driver가 dmaengine을 사용해 DMA controller를 추상화합니다.

#DMA, IRQ, Polling 비교

방식CPU 사용LatencyThroughput
Polling100%매우 낮음bus 한계
IRQ per byte높음 (overhead)낮음낮음
IRQ per buffer낮음buffer 단위보통
DMA + IRQ매우 낮음buffer 단위bus 한계
DMA chain~00 (CPU 안 봐도 됨)bus 한계

#자주 하는 실수

⚠️ DMA buffer를 stack에

void send_data(void) {
uint8_t buf[256]; // ← stack
fill(buf);
HAL_UART_Transmit_DMA(&huart, buf, 256);
return; // ← buf out of scope, DMA 진행 중
}

DMA buffer는 static, heap, 전역 중 하나에 둬야 합니다. Stack 위에 두면 함수 return 시점에 깨집니다.

⚠️ Cache flush 안 함

DMA가 cache stale data를 보거나 CPU가 옛 data를 읽게 됩니다. 매 transfer 전후로 clean/invalidate를 수행해야 합니다.

⚠️ MPU 영역 잘못 설정

region.Size = MPU_REGION_SIZE_4KB;

DMA buffer가 region 경계를 넘어가면 일부는 cacheable, 일부는 non-cacheable이 됩니다. 결과는 미정의 동작입니다.

⚠️ Burst size > slave 지원

Peripheral FIFO depth가 4인데 burst length가 16이면 slave가 4-cycle마다 backpressure를 걸어 throughput이 손실됩니다.

#정리

  • DMA 성능은 burst size, alignment, outstanding, bus contention으로 결정됩니다.
  • Scatter-gather와 chain으로 CPU 개입을 최소화합니다.
  • Double buffer로 throughput을 2배로 끌어올립니다.
  • Cortex-M7에서는 clean/invalidate가 필수이고 non-cacheable region도 선택지입니다.
  • 작은 copy는 CPU memcpy, 큰 copy와 peripheral은 DMA가 유리합니다.

다음 편은 DMA vs CPU 손익 분석을 다룹니다.

#관련 항목

Embedded Performance Engineering · 22 of 57

  1. 1Embedded Performance Engineering — 임베디드 성능 엔지니어링 시리즈 소개
  2. 2임베디드 성능 분석 방법론 — Measure → Analyze → Optimize 사이클
  3. 3성능 지표 정의 — Latency·Throughput·Utilization 분석
  4. 4성능 측정의 기본 — Wall-Clock·CPU Cycle·Instruction Count
  5. 5성능 데이터 통계적 분석 — Percentile·Histogram·평균의 함정
  6. 6실시간 성능 분석 — WCET·Jitter·Deadline Miss 측정
  7. 7임베디드 벤치마킹 기초 — 재현성·Warmup·노이즈 제거
  8. 8성능 모델링 — Amdahl·Gustafson·Roofline Model 적용
  9. 9프로파일링 기법 개요 — Sampling vs Instrumentation·PGO·LTO
  10. 10CPU 파이프라인 분석 — 5-stage·Cortex-M·Cortex-A 비교
  11. 11Pipeline Stall 분석 — Data·Structural·Control Hazard·Forwarding
  12. 12Branch Prediction 분석 — Static·2-bit·BTB·BHT·Mispredict 비용
  13. 13Speculative Execution 분석 — OoO·Reorder Buffer·Register Renaming
  14. 14CPU Cache 기초 — L1·L2·L3·Set Associative·Replacement Policy
  15. 15Cache Miss 3C Model 분석 — Compulsory·Capacity·Conflict
  16. 16Cache Line 최적화 — Alignment·Prefetch·False Sharing 처리
  17. 17메모리 대역폭 분석 — STREAM·Roofline·Bus Saturation 측정
  18. 18SIMD·NEON 활용 — 128-bit Vector·Auto-Vectorization·SVE/SVE2
  19. 19PMU·HPM 하드웨어 카운터 분석 — 정밀 성능 진단
  20. 20임베디드 Bus Architecture — AHB·AXI·CHI 진화와 5-Channel
  21. 21Bus Contention 진단 — Arbitration·QoS·Starvation 측정
  22. 22DMA 성능 최적화 — Burst·Scatter-Gather·Chain·Cache 일관성
  23. 23DMA vs CPU Copy 성능 비교 — Break-even·Setup Overhead 실측
  24. 24Interrupt Latency 분석 — 진입·종료·Tail-Chaining·Late Arrival
  25. 25Interrupt Storm 처리 — NAPI·Rate-Limit·Polling 전환
  26. 26MMIO 접근 성능 — Cache Policy·Write-Combining·Volatile·Barrier
  27. 27Peripheral Clock 분석 — PLL·Divider·Gating·DVFS
  28. 28Power vs Performance 트레이드오프 — DVFS·Race-to-Idle·Big.LITTLE
  29. 29Thermal Throttling 분석 — Junction Temp·Trip Point·냉각
  30. 30CXL Interconnect 분석 — AI 시대 메모리 대역폭 확장
  31. 31Concurrency 기초 — Concurrency vs Parallelism·Race·Memory Model
  32. 32False Sharing 진단 — Cache Line Ping-Pong·Padding·측정
  33. 33Lock Contention 분석 — Wait·Hold·Convoy·측정 기법
  34. 34Spinlock 성능 분석 — Spin-Wait vs Context Switch·Ticket·MCS
  35. 35Mutex 성능 분석 — Futex·Adaptive·Priority Inheritance
  36. 36Reader-Writer Lock 성능 — Reader/Writer Priority·RCU·Seqlock
  37. 37Lock-Free 자료구조 성능 — CAS·ABA·Hazard Pointer·Epoch Reclamation
  38. 38Memory Ordering 분석 — Acquire·Release·Seq-Cst·ARM Relaxed Model
  39. 39Cache Coherency 프로토콜 — MESI·MOESI·Snoop·Directory
  40. 40SMP 성능 분석 — Per-Core·Affinity·Load Balance·Scalability
  41. 41Linux perf 기초 — stat·record·report 활용
  42. 42Linux perf 고급 — Raw Event·Tracepoint·perf script
  43. 43ftrace 활용 — function·function_graph·latency tracer
  44. 44eBPF·bpftrace 동적 트레이싱 — 커널 무수정 관측
  45. 45Flamegraph 분석 — On-CPU·Off-CPU·Differential
  46. 46ARM DS·Lauterbach 분석 — Hardware Trace 전문 도구
  47. 47Bare-metal 프로파일링 — GPIO·DWT·SysTick·ITM 활용
  48. 48NVIDIA Nsight Systems — GPU·NPU 포함 시스템 분석
  49. 49모던 프로파일러 비교 — Tracy·Hotspot·uftrace·Coz
  50. 50연속 프로파일링 — Parca·Pixie·Pyroscope·Tetragon
  51. 51실전 사례 — ISR Latency 100µs Deadline Miss 추적
  52. 52실전 사례 — Matrix Multiply가 예상의 10배 느린 이유
  53. 53실전 사례 — 8-core가 4-core를 넘으면 throughput이 떨어지는 이유
  54. 54실전 사례 — 카메라 1080p 60fps가 30fps로 떨어지는 이유
  55. 55CXL.mem 지연·대역폭 실측 — Direct·Switch·Pooled 토폴로지 비교
  56. 56CXL 성능 프로파일링 도구 — cxl-cli·DAMON·perf-mem 활용
  57. 57실전 사례 — CXL.mem 추가로 LLM inference KV cache 처리량 회복