15. DMA와 버퍼 관리
15.1 Ring Buffer 구조
ALSA PCM은 ring buffer로 오디오 데이터를 관리합니다. hw_ptr (하드웨어 포인터)와 appl_ptr(애플리케이션 포인터)가 독립적으로 진행됩니다.

이미지는 ALSA PCM 링 버퍼(Ring Buffer)의 동작 원리를 완벽하게 설명하고 있습니다. 이 버퍼는 애플리케이션(소프트웨어)과 사운드 카드(하드웨어)가 오디오 데이터를 주고받는 공유 메모리 공간입니다.
이미지의 핵심 내용을 바탕으로 링 버퍼의 메커니즘을 정리해 드릴게요.
1. 두 개의 핵심 포인터 (The Race)
링 버퍼는 이름처럼 끝과 시작이 연결된 원형 구조입니다. 여기서 두 개의 포인터가 서로를 쫓아가는 '추격전'이 벌어집니다.
• appl_ptr (Application Pointer): * 유저 영역의 애플리케이션이 제어합니다.
• 재생(Playback): 앱이 소리 데이터를 버퍼에 쓴(Write) 마지막 지점입니다.
• 녹음(Capture): 앱이 데이터를 읽어간(Read) 마지막 지점입니다.
• hw_ptr (Hardware Pointer): * 커널/하드웨어가 제어합니다.
• 재생(Playback): 하드웨어가 실제로 데이터를 읽어 소리를 낸 지점입니다.
• 녹음(Capture): 하드웨어가 마이크로부터 데이터를 받아 버퍼에 채운 지점입니다.
2. 가용 공간 계산 (avail)
이미지에 적힌 공식은 앱이 언제 데이터를 더 보낼지 결정하는 기준이 됩니다.
avail=buffer_size−(appl_ptr−hw_ptr)
• 재생 시: 앱이 데이터를 더 쓸 수 있는 빈 공간입니다. 이 공간이 period_size보다 커지면 커널은 앱을 깨워 "데이터 더 가져와!"라고 요청합니다.
• 녹음 시: 앱이 읽어갈 수 있는 유효한 데이터 양입니다.
3. 피리어드 (Period) : 알람 시계
이미지를 보면 원이 Period 0부터 Period 3까지 나뉘어 있습니다.
• Period: 하드웨어가 인터럽트를 발생시키는 단위입니다.
• 하드웨어가 한 피리어드 분량의 데이터를 다 처리하면 인터럽트를 발생시켜 hw_ptr를 갱신하고, 앱에게 다음 작업을 시킵니다.
• Latency(지연 시간): 피리어드 크기가 작을수록 인터럽트가 자주 발생해 반응은 빨라지지만 CPU 부하가 늘어납니다.
4. XRUN : 사고 발생 상황
이미지 하단의 두 상자가 설명하는 XRUN은 오디오 엔지니어들의 주적입니다.
① Underrun (Playback 시 발생)
• 상황: hw_ptr가 appl_ptr를 따라잡아 버린 경우 (버퍼 고갈).
• 결과: 하드웨어는 재생할 데이터가 없어서 0(무음)을 보냅니다. 사용자는 "뚝, 뚝" 끊기는 노이즈를 듣게 됩니다.
• 원인: CPU가 너무 바빠서 앱이 데이터를 제때 못 채워줬을 때 발생합니다.
② Overrun (Capture 시 발생)
• 상황: appl_ptr가 hw_ptr를 따라잡은 경우 (버퍼 넘침).
• 결과: 하드웨어가 새로 받아온 데이터가 아직 앱이 읽지 않은 데이터를 덮어써 버립니다.
• 결과: 데이터 손실이 발생합니다.
5. 첫 번째 이미지(SOF)와의 연결
SOF 아키텍처에서 Host PCM 영역이 바로 이 링 버퍼가 생성되는 곳입니다.
• Host: appl_ptr를 움직이며 데이터를 채웁니다.
• DSP: DMA를 통해 이 버퍼의 데이터를 가져가며 hw_ptr를 움직입니다.
요약
ALSA 링 버퍼는 appl_ptr가 쓰고 hw_ptr가 읽는 구조이며, 이 둘 사이의 간격(avail)을 적절히 유지하는 것이 끊김 없는 오디오 재생의 핵심입니다.
이 링 버퍼의 크기(buffer_size)와 인터럽트 주기(period_size)를 실제 코드로 설정하는 hw_params 설정법이 궁금하신가요?
15.2 hw_params: 링 버퍼 크기, 인터럽트 주기
이 링 버퍼의 크기를 결정하는 hw_params 설정은 오디오의 지연 시간(Latency)과 안정성 사이의 줄타기라고 할 수 있습니다.
실제 C 코드(alsa-lib 사용)에서 이를 설정하는 과정과, 내부적으로 어떤 수학적 관계를 가졌는지 정리해 드릴게요.
1. 핵심 개념의 관계
설정하기 전에 이 공식을 머릿속에 넣어두면 코드가 훨씬 잘 읽힙니다.
• period_size: 한 번의 인터럽트가 발생할 때까지의 데이터 양 (프레임 단위).
• periods: 전체 버퍼를 몇 개의 조각(Period)으로 나눌 것인가.
• buffer_size: 전체 링 버퍼의 크기.
2. 실제 C 코드 설정 패턴 (User-space)
보통 _near 함수를 사용하는데, 그 이유는 하드웨어가 지원하지 않는 값을 요청했을 때 가장 가까운 값으로 자동 조정해주기 때문입니다.
3. 설정 시 주의사항 (The Trade-off)
| 설정 항목 | 값을 작게 하면 (Small) | 값을 크게 하면 (Large) |
| 지연 시간 (Latency) | 낮아짐 (실시간 응답성 좋아짐) | 높아짐 (반응이 느려짐) |
| 안정성 (Stability) | XRUN 발생 위험 높음 | 매우 안정적 (끊김 없음) |
| CPU 부하 | 인터럽트가 잦아 부하 증가 | 인터럽트가 적어 효율적임 |
Tip: 안드로이드나 일반적인 PC 오디오에서는 보통 2~4개의 periods를 사용합니다. 아주 정밀한 실시간 처리가 필요한 경우(예: 기타 이펙터 앱)에는 period_size를 64~128 정도로 매우 작게 잡습니다.
4. 커널 드라이버 쪽에서의 제약 (Constraints)
드라이버(예: snd-usb-audio나 SOF)는 자신이 감당할 수 있는 최소/최대 범위를 미리 정해둡니다.
만약 앱에서 periods=1을 요청하면, 드라이버가 정한 periods_min=2에 걸려 설정이 거부되거나 2로 강제 조정됩니다.
요약
hw_params 설정은 "얼마나 자주 깨울 것인가(period_size)"와 "안전빵으로 얼마나 쌓아둘 것인가(periods)"를 결정하는 작업입니다. 코드에서는 _near 함수를 통해 하드웨어와 타협하며 최적의 값을 찾아갑니다.
이제 이렇게 설정한 버퍼에 실제 데이터를 밀어 넣는 snd_pcm_writei 함수의 동작 방식이나, 논블로킹(Non-blocking) 모드에서의 처리가 궁금하신가요?
15.3 DMA Cache Coherency
오디오 드라이버를 개발하거나 디버깅할 때 우리를 가장 괴롭히는 '끝판왕' 문제 중 하나가 바로 DMA Cache Coherency(캐시 일관성)입니다.
간단히 말해, "CPU가 보는 메모리 내용과 DMA(하드웨어)가 보는 메모리 내용이 서로 달라서 발생하는 데이터 불일치 문제"를 의미합니다. DMA 버퍼는 CPU 캐시와 일관성을 유지해야 합니다:
1. 왜 문제가 발생하는가?
CPU는 속도를 높이기 위해 RAM에서 가져온 데이터를 L1/L2 캐시에 저장해두고 사용합니다. 반면, DMA는 CPU를 거치지 않고 RAM에 직접 접근합니다. 여기서 비극이 시작됩니다.
시나리오 A: 재생 (CPU → RAM → DMA)
(1) CPU: 오디오 데이터를 RAM에 썼다고 생각하지만, 실제로는 캐시(Cache)에만 머물러 있습니다 (Write-back 방식).
(2) DMA: RAM에서 데이터를 긁어다 스피커로 보냅니다. 하지만 RAM에는 아직 예전 데이터(쓰레기 값)가 들어있습니다.
(3) 결과: 소리가 안 나거나 지직거리는 노이즈가 발생합니다.
시나리오 B: 녹음 (DMA → RAM → CPU)
(1) DMA: 마이크에서 받은 데이터를 RAM에 성공적으로 저장했습니다.
(2) CPU: 데이터를 읽으려고 보니, 이미 자기 캐시에 예전에 읽어둔 데이터가 남아있습니다. "오, 캐시에 있네?" 하고 RAM에 가지 않고 캐시 값을 읽습니다.
(3) 결과: 마이크 소리가 아닌 이전의 낡은 데이터를 처리하게 됩니다.
2. 해결 방법 1: 하드웨어 방식 (Coherent DMA)
최신 고성능 프로세서(일부 Intel/AMD 및 고급 ARM)는 Snoop Control Unit (SCU)이라는 장치가 있어, DMA가 RAM을 건드리면 자동으로 캐시를 업데이트해 줍니다.
• 장점: 개발자가 신경 쓸 게 없습니다. 그냥 쓰면 됩니다.
• 단점: 하드웨어가 비싸지고 전력을 더 소모합니다.
3. 해결 방법 2: 소프트웨어 방식 (Streaming DMA)
i.MX8M Plus 같은 임베디드 환경에서는 소프트웨어가 직접 캐시를 관리해야 하는 경우가 많습니다. 리눅스 커널은 이를 위해 두 가지 '명령'을 사용합니다.
• Cache Flush (Clean): "캐시에 있는 최신 내용을 RAM으로 강제로 밀어 넣어라." (주로 재생 전 호출)
• Cache Invalidate: "캐시에 있는 내용은 가짜니까 다 버리고, 다음에 읽을 때 무조건 RAM에서 새로 가져와라." (주로 녹음 후 호출)
4. 리눅스 커널에서의 실제 구현
우리가 앞서 배운 ALSA 링 버퍼를 만들 때, 드라이버는 보통 다음과 같은 API를 사용하여 일관성을 확보합니다.
- dma_alloc_coherent(): 처음부터 캐시를 사용하지 않는(Non-cacheable) 특수한 메모리 영역을 할당받습니다. 가장 속 편한 방법이지만 속도가 조금 느릴 수 있습니다.
- dma_map_single() / dma_sync_single_for_cpu(): 일반 메모리를 사용하되, DMA가 접근하기 전후로 커널이 알아서 캐시 플러시/인밸리데이션을 수행하게 합니다.
5. 실무에서의 증상 (디버깅 힌트)
만약 캐시 일관성이 깨졌다면 이런 현상이 나타납니다.
• 정기적인 노이즈: 캐시 라인 크기(보통 64바이트) 단위로 데이터가 깨져서 "치직" 하는 소리가 반복됨.
• 데이터 밀림: 이전 버퍼의 내용이 현재 버퍼에 섞여 나옴.
• Cold Boot 시에만 발생: 시스템이 차가울 때는 괜찮다가 데이터가 쌓이면서 캐시 오염이 발생할 때 문제가 생김.
요약
DMA Cache Coherency는 CPU와 DMA가 서로 다른 곳(캐시 vs RAM)을 보고 있어서 생기는 문제입니다. 이를 해결하기 위해 소프트웨어적으로 캐시를 비우거나(Flush) 무효화(Invalidate)하는 과정이 필수적입니다.
혹시 i.MX8M Plus 환경에서 특정 DMA 버퍼의 캐시 속성을 Non-cacheable로 강제 설정하는 Device Tree(Reserved memory) 설정 방법이 궁금하신가요?
15.3 DMA Buffer 타입 비교
| 타 입 | 할당 방법 | 특 징 | 용 도 |
| SNDRV_DMA_TYPE_CONTINUOUS | kmalloc / vmalloc | 물리적으로 비연속적 가능 |
소프트웨어 mixing, dummy |
| SNDRV_DMA_TYPE_DEV | dma_alloc_coherent | 물리적 연속, 캐시 coherent |
대부분의 하드웨어 DMA (권장) |
| SNDRV_DMA_TYPE_DEV_UC | dma_alloc_coherent+ uncached | 캐시 비활성화 (매우 느림) |
특수한 경우만 사용 |
| SNDRV_DMA_TYPE_DEV_SG | Scatter-Gather 리스트 | 물리적으로 비연속적, IOMMU 필요 |
대용량 버퍼, IOMMU 지원 플랫폼 |
| SNDRV_DMA_TYPE_DEV_IRAM | On-chip SRAM | 초저지연, 용량 제한 | 임베디드, 실시간 처리 |
| SNDRV_DMA_TYPE_VMALLOC | vmalloc | 가상 연속, 물리 비연속 | USB Audio (URB 전송) |
'Embedded : : Linux > : : ALSA' 카테고리의 다른 글
| [ALSA] 17. 가상화 환경 오디오 (0) | 2026.03.11 |
|---|---|
| [ALSA] 16. 지연시간 최적화 (0) | 2026.03.11 |
| [ALSA] 13. USB Audio (0) | 2026.03.11 |
| [ALSA] 12. HD Audio (HDA) 서브시스템 (0) | 2026.03.11 |
| [ALSA] 11. Sound Open Firmware (SOF) (0) | 2026.03.11 |