DMA Engine (DMA 컨트롤러 프레임워크)
DMA Engine 프레임워크를 컨트롤러 하드웨어 추상화와 클라이언트 드라이버 재사용 관점에서 심층 분석합니다. Provider/Consumer API 경계, dma_device/dma_chan/dma_async_tx_descriptor 생명주기, Slave SG/Cyclic/Memcpy/Interleaved 전송 모델의 선택 기준, async_tx 오프로딩 활용, completion/interrupt 처리와 backlog 제어, dmatest 기반 검증 절차, 오디오·네트워크·산업장치 실전 적용 패턴, 지연시간·처리량 튜닝과 오류 복구까지 DMA 컨트롤러 드라이버 품질 향상에 필요한 내용을 다룹니다.
Documentation/driver-api/dmaengine/ 디렉터리에 있습니다. provider.rst(컨트롤러 드라이버), client.rst(클라이언트 API), dmatest.rst(테스트 모듈)를 참조하십시오.
핵심 요약
- DMA Engine — DMA 컨트롤러 하드웨어를 추상화하는 커널 프레임워크로,
drivers/dma/에 위치합니다. - Provider vs Consumer — Provider는 DMA 컨트롤러 드라이버(HW 제어), Consumer는 이를 사용하는 클라이언트(SPI, I2C, 오디오 등)입니다.
- 핵심 자료구조 —
dma_device(컨트롤러),dma_chan(채널),dma_async_tx_descriptor(전송 디스크립터)가 핵심입니다. - 전송 유형 — memcpy(메모리 복사), slave(주변장치↔메모리), cyclic(순환 DMA), interleaved(스트라이드) 등을 지원합니다.
단계별 이해
- DMA Engine이 필요한 이유 — SoC에는 여러 DMA 컨트롤러가 존재하고, 각각 다른 레지스터/프로토콜을 사용합니다.
DMA Engine 프레임워크는 이를 통일된 API로 추상화하여 클라이언트가 하드웨어 세부사항을 몰라도 DMA를 사용할 수 있게 합니다.
- 채널 요청 —
dma_request_chan()으로 DMA 채널을 획득합니다. Device Tree의dmas프로퍼티로 매핑됩니다. - 전송 설정 —
dmaengine_slave_config()로 버스 폭, 버스트 크기 등을 설정합니다. - 디스크립터 준비 —
dmaengine_prep_slave_sg()등으로 전송 디스크립터를 생성합니다. - 전송 실행 —
dmaengine_submit()으로 큐에 넣고,dma_async_issue_pending()으로 실제 전송을 시작합니다.
DMA Engine 개요
데이터 전송 방식은 PIO(Programmed I/O) → 인터럽트 구동 I/O → DMA → DMA Engine으로 발전해 왔습니다.
데이터 전송 발전 과정
| 단계 | 방식 | 특징 | 한계 |
|---|---|---|---|
| 1 | PIO | CPU가 직접 I/O 포트로 바이트 전송 | CPU 100% 점유, 느림 |
| 2 | 인터럽트 구동 | 디바이스 준비 시 인터럽트로 알림 | 여전히 CPU가 데이터 복사 |
| 3 | DMA (하드웨어) | DMA 컨트롤러가 CPU 대신 전송 | 컨트롤러마다 다른 레지스터/프로토콜 |
| 4 | DMA Engine | 커널 프레임워크가 DMA 컨트롤러 추상화 | 통일된 API, 다양한 전송 유형 지원 |
dmaengine 서브시스템의 역할
dmaengine 서브시스템은 다음을 제공합니다:
- 통일된 API — 클라이언트 드라이버가 DMA 컨트롤러 하드웨어를 직접 다루지 않고 표준 API를 사용
- 채널 관리 — DMA 채널 할당/해제, 공유/전용 채널 정책
- 전송 스케줄링 — 디스크립터 큐잉, 의존성 체인, 완료 콜백
- Device Tree 통합 —
dmas/dma-names프로퍼티로 자동 바인딩
디렉터리 구조
/* 핵심 프레임워크 */
drivers/dma/dmaengine.c /* 코어 로직 */
drivers/dma/of-dma.c /* Device Tree DMA 매핑 */
drivers/dma/acpi-dma.c /* ACPI DMA 매핑 */
drivers/dma/virt-dma.c /* 가상 디스크립터 헬퍼 */
include/linux/dmaengine.h /* 주요 헤더 */
/* 컨트롤러 드라이버 예시 */
drivers/dma/pl330.c /* ARM PL330 */
drivers/dma/ioat/ /* Intel IOAT (Crystal Beach) */
drivers/dma/ti/ /* TI eDMA, K3 UDMA */
drivers/dma/dw/ /* Synopsys DesignWare DMA */
drivers/dma/stm32-dma.c /* STM32 DMA */
drivers/dma/fsl-edma.c /* Freescale/NXP eDMA */
아키텍처
Provider vs Consumer 모델
DMA Engine은 Provider-Consumer 패턴을 사용합니다:
- Provider (DMA 컨트롤러 드라이버) —
struct dma_device를 등록하고, 하드웨어별 콜백을 구현합니다. - Consumer (클라이언트 드라이버) — SPI, I2C, UART, 오디오 등 주변장치 드라이버가 DMA 채널을 요청하여 데이터를 전송합니다.
핵심 자료구조
struct dma_device
DMA 컨트롤러 전체를 표현합니다. Provider가 이 구조체를 채워서 등록합니다.
struct dma_device {
struct list_head channels; /* 이 컨트롤러의 채널 목록 */
unsigned int chancnt; /* 채널 수 */
dma_cap_mask_t cap_mask; /* 지원 capability 비트마스크 */
struct device *dev; /* 부모 디바이스 */
/* Provider 콜백 함수 포인터 */
int (*device_alloc_chan_resources)(struct dma_chan *chan);
void (*device_free_chan_resources)(struct dma_chan *chan);
struct dma_async_tx_descriptor *(*device_prep_dma_memcpy)(...);
struct dma_async_tx_descriptor *(*device_prep_slave_sg)(...);
struct dma_async_tx_descriptor *(*device_prep_dma_cyclic)(...);
enum dma_status (*device_tx_status)(...);
void (*device_issue_pending)(struct dma_chan *chan);
int (*device_terminate_all)(struct dma_chan *chan);
...
};
struct dma_chan
DMA 채널 하나를 표현합니다. 클라이언트는 채널을 획득하여 전송을 수행합니다.
struct dma_chan {
struct dma_device *device; /* 소속 DMA 컨트롤러 */
dma_cookie_t cookie; /* 마지막 제출된 쿠키 */
dma_cookie_t completed_cookie; /* 마지막 완료된 쿠키 */
int chan_id; /* 채널 번호 */
struct dma_chan_dev *dev; /* sysfs 디바이스 */
const char *name; /* consumer 이름 */
struct list_head device_node; /* dma_device->channels 리스트 */
...
};
struct dma_async_tx_descriptor
하나의 DMA 전송을 표현하는 디스크립터입니다. dmaengine_prep_*()가 반환합니다.
struct dma_async_tx_descriptor {
dma_cookie_t cookie; /* 전송 식별 쿠키 */
struct dma_chan *chan; /* 전송할 채널 */
dma_async_tx_callback callback; /* 완료 콜백 */
void *callback_param; /* 콜백 파라미터 */
struct dma_async_tx_descriptor *next; /* 체인 연결 */
enum dma_ctrl_flags flags; /* DMA_PREP_INTERRUPT 등 */
...
};
struct dma_slave_config
슬레이브 전송에 필요한 설정 정보를 담습니다.
struct dma_slave_config {
enum dma_transfer_direction direction; /* DMA_MEM_TO_DEV 등 */
phys_addr_t src_addr; /* 소스 FIFO 물리 주소 */
phys_addr_t dst_addr; /* 목적지 FIFO 물리 주소 */
enum dma_slave_buswidth src_addr_width; /* 1/2/4/8 바이트 */
enum dma_slave_buswidth dst_addr_width;
u32 src_maxburst; /* 소스 버스트 크기 */
u32 dst_maxburst; /* 목적지 버스트 크기 */
bool device_fc; /* 디바이스 흐름 제어 */
...
};
Provider API (컨트롤러 드라이버)
DMA 컨트롤러 드라이버(Provider)는 struct dma_device를 채운 뒤 dma_async_device_register()로 프레임워크에 등록합니다.
필수 콜백 구현
| 콜백 | 역할 | 필수 여부 |
|---|---|---|
device_alloc_chan_resources | 채널 리소스 할당 (디스크립터 풀 등) | 필수 |
device_free_chan_resources | 채널 리소스 해제 | 필수 |
device_prep_dma_memcpy | memcpy 전송 디스크립터 생성 | DMA_MEMCPY 지원 시 |
device_prep_slave_sg | slave scatter-gather 디스크립터 생성 | DMA_SLAVE 지원 시 |
device_prep_dma_cyclic | cyclic 전송 디스크립터 생성 | DMA_CYCLIC 지원 시 |
device_prep_interleaved_dma | interleaved 전송 디스크립터 생성 | DMA_INTERLEAVE 지원 시 |
device_issue_pending | 보류 중인 전송을 하드웨어에 전달 | 필수 |
device_tx_status | 전송 상태 조회 (완료/진행 중) | 필수 |
device_terminate_all | 진행 중인 모든 전송 취소 | 권장 |
device_config | slave 설정 적용 | DMA_SLAVE 지원 시 |
device_pause/device_resume | 전송 일시정지/재개 | 선택 |
등록 / 해제
/* 등록 */
int dma_async_device_register(struct dma_device *device);
/* 해제 */
void dma_async_device_unregister(struct dma_device *device);
간단한 Provider 프레임
static int my_dma_probe(struct platform_device *pdev)
{
struct my_dma_dev *mydev;
struct dma_device *dma_dev;
mydev = devm_kzalloc(&pdev->dev, sizeof(*mydev), GFP_KERNEL);
dma_dev = &mydev->dma_dev;
/* capability 설정 */
dma_cap_set(DMA_SLAVE, dma_dev->cap_mask);
dma_cap_set(DMA_CYCLIC, dma_dev->cap_mask);
/* 콜백 등록 */
dma_dev->dev = &pdev->dev;
dma_dev->device_alloc_chan_resources = my_alloc_chan_resources;
dma_dev->device_free_chan_resources = my_free_chan_resources;
dma_dev->device_prep_slave_sg = my_prep_slave_sg;
dma_dev->device_prep_dma_cyclic = my_prep_dma_cyclic;
dma_dev->device_config = my_device_config;
dma_dev->device_issue_pending = my_issue_pending;
dma_dev->device_tx_status = my_tx_status;
dma_dev->device_terminate_all = my_terminate_all;
/* 채널 초기화 */
INIT_LIST_HEAD(&dma_dev->channels);
for (int i = 0; i < NUM_CHANNELS; i++) {
mydev->chans[i].vchan.desc_free = my_desc_free;
vchan_init(&mydev->chans[i].vchan, dma_dev);
}
return dma_async_device_register(dma_dev);
}
drivers/dma/virt-dma.h의 vchan_init(), vchan_tx_submit(), vchan_find_desc() 등을 활용하여 디스크립터 관리를 단순화합니다.DMA_PRIVATE 채널
DMA_PRIVATE 플래그를 설정하면 해당 컨트롤러의 채널은 dma_request_chan()(Device Tree/ACPI 기반)으로만 요청할 수 있고, 범용 memcpy용으로 할당되지 않습니다. 대부분의 SoC DMA 컨트롤러가 이 모드를 사용합니다.
dma_cap_set(DMA_PRIVATE, dma_dev->cap_mask);
Consumer API (클라이언트)
채널 요청
/* 권장 API: Device Tree/ACPI 기반 채널 요청 */
struct dma_chan *dma_request_chan(struct device *dev, const char *name);
/* 채널 해제 */
void dma_release_channel(struct dma_chan *chan);
Device Tree 바인딩
/* Device Tree 예시 */
spi1: spi@48030000 {
compatible = "ti,omap4-mcspi";
dmas = <&edma 42 0>, <&edma 43 0>;
dma-names = "tx0", "rx0";
};
클라이언트 드라이버에서:
struct dma_chan *tx_chan = dma_request_chan(dev, "tx0");
struct dma_chan *rx_chan = dma_request_chan(dev, "rx0");
if (IS_ERR(tx_chan))
return PTR_ERR(tx_chan);
슬레이브 설정
struct dma_slave_config cfg = {
.direction = DMA_MEM_TO_DEV,
.dst_addr = spi_base + SPI_TX_REG, /* FIFO 물리 주소 */
.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE,
.dst_maxburst = 16,
};
dmaengine_slave_config(tx_chan, &cfg);
디스크립터 준비
Slave Scatter-Gather
struct dma_async_tx_descriptor *desc;
desc = dmaengine_prep_slave_sg(chan, sg, nents,
DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
Cyclic DMA (오디오 등)
desc = dmaengine_prep_dma_cyclic(chan,
buf_addr, /* DMA 버퍼 주소 */
buf_len, /* 전체 버퍼 크기 */
period_len, /* 한 주기 크기 */
DMA_MEM_TO_DEV, /* 방향 */
DMA_PREP_INTERRUPT);
Memcpy
desc = dmaengine_prep_dma_memcpy(chan, dst_dma, src_dma, len,
DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
전송 제출과 실행
/* 1. 콜백 설정 */
desc->callback = my_dma_complete_callback;
desc->callback_param = my_data;
/* 2. 큐에 제출 (아직 전송 시작 안 함) */
dma_cookie_t cookie = dmaengine_submit(desc);
/* 3. 보류 중인 모든 전송을 하드웨어에 전달 */
dma_async_issue_pending(chan);
dmaengine_submit()은 전송을 큐에 넣기만 합니다. 실제 전송은 dma_async_issue_pending()을 호출해야 시작됩니다. 이 2단계 분리는 여러 전송을 배치로 제출한 뒤 한 번에 실행하기 위함입니다.완료 확인
/* 방법 1: 콜백 (비동기) */
static void my_dma_complete_callback(void *param)
{
struct my_data *data = param;
complete(&data->dma_done);
}
/* 방법 2: 폴링 */
enum dma_status status;
struct dma_tx_state state;
status = dma_async_is_tx_complete(chan, cookie, NULL, &state);
/* status: DMA_COMPLETE, DMA_IN_PROGRESS, DMA_ERROR, DMA_PAUSED */
/* 방법 3: 동기 대기 */
dma_wait_for_async_tx(desc);
전체 Consumer 흐름 요약
/* ① 채널 요청 */
chan = dma_request_chan(dev, "rx");
/* ② 슬레이브 설정 */
dmaengine_slave_config(chan, &cfg);
/* ③ 디스크립터 준비 */
desc = dmaengine_prep_slave_sg(chan, sg, nents, dir, flags);
/* ④ 콜백 등록 */
desc->callback = my_callback;
/* ⑤ 제출 */
cookie = dmaengine_submit(desc);
/* ⑥ 실행 */
dma_async_issue_pending(chan);
/* ⑦ 완료 대기 */
wait_for_completion(&done);
/* ⑧ 채널 해제 (종료 시) */
dmaengine_terminate_sync(chan);
dma_release_channel(chan);
전송 유형
DMA Capability 유형
| Capability | 매크로 | 설명 | 사용 사례 |
|---|---|---|---|
| Memory Copy | DMA_MEMCPY | 메모리 → 메모리 복사 | Intel IOAT memcpy offload, 대용량 메모리 복사 |
| Memory Set | DMA_MEMSET | 메모리를 특정 값으로 채움 | 버퍼 초기화 |
| Slave | DMA_SLAVE | 디바이스 ↔ 메모리 (scatter-gather) | SPI, I2C, UART 데이터 전송 |
| Cyclic | DMA_CYCLIC | 순환 버퍼 전송 (링 버퍼) | 오디오 PCM 재생/녹음, ADC 연속 샘플링 |
| Interleaved | DMA_INTERLEAVE | 스트라이드 패턴 전송 | 2D DMA, 프레임버퍼 전송 |
| XOR | DMA_XOR | XOR 연산 (RAID5 패리티) | async_tx RAID5 가속 |
| PQ | DMA_PQ | P+Q 연산 (RAID6) | async_tx RAID6 가속 |
전송 방향
| 방향 | 매크로 | 설명 |
|---|---|---|
| 메모리 → 메모리 | DMA_MEM_TO_MEM | memcpy, memset 전송 |
| 메모리 → 디바이스 | DMA_MEM_TO_DEV | TX: RAM에서 디바이스 FIFO로 |
| 디바이스 → 메모리 | DMA_DEV_TO_MEM | RX: 디바이스 FIFO에서 RAM으로 |
| 디바이스 → 디바이스 | DMA_DEV_TO_DEV | 디바이스 간 직접 전송 (드문 경우) |
전송 플래그
| 플래그 | 설명 |
|---|---|
DMA_PREP_INTERRUPT | 전송 완료 시 인터럽트/콜백 발생 |
DMA_CTRL_ACK | 프레임워크가 디스크립터를 재사용 가능하게 표시 |
DMA_PREP_CMD | 명령 DMA 전송 (특수 하드웨어) |
DMA_PREP_REPEAT | 반복 전송 (cyclic과 함께 사용) |
async_tx API
async_tx는 DMA Engine 위에 구축된 비동기 메모리 연산 프레임워크로, 주로 소프트웨어 RAID(md)에서 XOR/PQ 패리티 계산을 가속합니다.
주요 함수
| 함수 | 연산 | 용도 |
|---|---|---|
async_memcpy() | 메모리 복사 | DMA 가속 memcpy |
async_xor() | XOR | RAID5 패리티 계산 |
async_pq() | P+Q (Reed-Solomon) | RAID6 패리티 계산 |
async_syndrome_val() | 신드롬 검증 | RAID6 데이터 무결성 확인 |
async_xor_val() | XOR 검증 | RAID5 패리티 검증 |
의존성 체인
async_tx는 여러 연산을 체인으로 연결할 수 있습니다. 각 연산은 이전 연산의 dma_async_tx_descriptor를 의존성으로 받습니다.
struct dma_async_tx_descriptor *tx;
/* 첫 번째 XOR 연산 */
tx = async_xor(dest, srcs, 0, count, len,
&submit);
/* 두 번째 연산: 첫 번째 완료 후 실행 */
submit.depend_tx = tx;
tx = async_memcpy(dst2, src2, 0, 0, len2,
&submit);
소프트웨어 폴백
DMA Engine에 XOR/PQ를 지원하는 채널이 없으면, async_tx는 자동으로 CPU 기반 소프트웨어 구현으로 폴백합니다. 이 투명한 폴백 덕분에 RAID 코드는 하드웨어 유무와 관계없이 동일한 API를 사용할 수 있습니다.
주요 DMA 컨트롤러
| 컨트롤러 | 아키텍처 | 커널 드라이버 | 특징 |
|---|---|---|---|
| Intel IOAT (Crystal Beach) | x86 | drivers/dma/ioat/ | memcpy offload, DCA (Direct Cache Access), 서버급 |
| ARM PL330 (DMA-330) | ARM SoC (Samsung, etc.) | drivers/dma/pl330.c | 8채널, 마이크로 코드 기반, Exynos/RK3399 등 |
| TI eDMA3 | TI AM335x/AM57x | drivers/dma/ti/edma.c | 이벤트 트리거, 64채널, PaRAM 디스크립터 |
| TI K3 UDMA | TI AM62x/AM64x | drivers/dma/ti/k3-udma.c | 패킷 기반 DMA, NAVSS, 링 가속기 |
| NXP eDMA | i.MX8/LPC | drivers/dma/fsl-edma.c | 최대 64채널, 주변장치 전용 |
| STM32 DMA | STM32 MCU | drivers/dma/stm32-dma.c | Request Mux (DMAMUX), FIFO, 이중 버퍼 |
| Synopsys DW-DMA | 다양한 SoC | drivers/dma/dw/ | Multi-master, 블록 체인, Intel Baytrail 등 |
| Qualcomm BAM | Snapdragon | drivers/dma/qcom/bam_dma.c | Pipe 기반, SPI/UART/I2C 연결 |
Device Tree 바인딩 예시 (PL330)
pdma0: dma-controller@12680000 {
compatible = "arm,pl330", "arm,primecell";
reg = <0x12680000 0x1000>;
interrupts = <GIC_SPI 35 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clock CLK_PDMA0>;
clock-names = "apb_pclk";
#dma-cells = <1>;
};
DMA Engine과 DMA 매핑 관계
DMA Engine과 DMA 매핑 API(DMA 심화)는 서로 다른 계층입니다:
| 구분 | DMA 매핑 API | DMA Engine |
|---|---|---|
| 관심사 | 메모리 주소 변환, 캐시 일관성 | DMA 컨트롤러 추상화, 채널 관리 |
| 핵심 API | dma_map_sg(), dma_alloc_coherent() | dmaengine_prep_*(), dmaengine_submit() |
| 헤더 | linux/dma-mapping.h | linux/dmaengine.h |
| 위치 | kernel/dma/ | drivers/dma/ |
| IOMMU | 직접 다룸 | 내부적으로 매핑 API 사용 |
소프트웨어 스택 계층
dma_map_sg()를 호출해야 할 수 있습니다.실전 예제
SPI DMA 전송 (Slave SG)
static int spi_dma_transfer(struct spi_device *spi,
void *buf, size_t len)
{
struct spi_controller *ctlr = spi->controller;
struct dma_chan *chan = ctlr->dma_tx;
struct dma_async_tx_descriptor *desc;
struct scatterlist sg;
DECLARE_COMPLETION_ONSTACK(done);
/* scatter-gather 리스트 준비 */
sg_init_one(&sg, buf, len);
dma_map_sg(ctlr->dma_tx->device->dev, &sg, 1,
DMA_MEM_TO_DEV);
/* 디스크립터 생성 */
desc = dmaengine_prep_slave_sg(chan, &sg, 1,
DMA_MEM_TO_DEV,
DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
if (!desc)
return -ENOMEM;
/* 콜백 등록 및 전송 */
desc->callback = spi_dma_complete;
desc->callback_param = &done;
dmaengine_submit(desc);
dma_async_issue_pending(chan);
/* 완료 대기 */
wait_for_completion_timeout(&done,
msecs_to_jiffies(1000));
dma_unmap_sg(ctlr->dma_tx->device->dev, &sg, 1,
DMA_MEM_TO_DEV);
return 0;
}
오디오 Cyclic DMA (ALSA PCM)
static int audio_dma_trigger(struct snd_pcm_substream *ss,
int cmd)
{
struct dma_chan *chan = ss->runtime->private_data;
struct dma_async_tx_descriptor *desc;
dma_addr_t buf_addr = ss->runtime->dma_addr;
size_t buf_len = snd_pcm_lib_buffer_bytes(ss);
size_t period_len = snd_pcm_lib_period_bytes(ss);
switch (cmd) {
case SNDRV_PCM_TRIGGER_START:
desc = dmaengine_prep_dma_cyclic(chan,
buf_addr, buf_len, period_len,
DMA_MEM_TO_DEV,
DMA_PREP_INTERRUPT);
desc->callback = audio_period_elapsed;
desc->callback_param = ss;
dmaengine_submit(desc);
dma_async_issue_pending(chan);
break;
case SNDRV_PCM_TRIGGER_STOP:
dmaengine_terminate_async(chan);
break;
}
return 0;
}
static void audio_period_elapsed(void *param)
{
struct snd_pcm_substream *ss = param;
snd_pcm_period_elapsed(ss);
}
memcpy offload (Intel IOAT)
struct dma_chan *chan;
struct dma_async_tx_descriptor *tx;
dma_addr_t src_dma, dst_dma;
dma_cookie_t cookie;
DECLARE_COMPLETION_ONSTACK(cmp);
/* 범용 memcpy 채널 요청 */
dma_cap_mask_t mask;
dma_cap_zero(mask);
dma_cap_set(DMA_MEMCPY, mask);
chan = dma_request_channel(mask, NULL, NULL);
/* DMA 주소 매핑 */
src_dma = dma_map_single(chan->device->dev,
src, len, DMA_TO_DEVICE);
dst_dma = dma_map_single(chan->device->dev,
dst, len, DMA_FROM_DEVICE);
/* memcpy 디스크립터 생성 */
tx = dmaengine_prep_dma_memcpy(chan, dst_dma, src_dma,
len, DMA_PREP_INTERRUPT);
tx->callback = memcpy_done;
tx->callback_param = &cmp;
cookie = dmaengine_submit(tx);
dma_async_issue_pending(chan);
wait_for_completion(&cmp);
dma_unmap_single(chan->device->dev, src_dma, len, DMA_TO_DEVICE);
dma_unmap_single(chan->device->dev, dst_dma, len, DMA_FROM_DEVICE);
dma_release_channel(chan);
완전한 UART DMA 드라이버 예제
초기화부터 정리까지 포함된 실무 수준의 UART RX DMA 구현입니다.
struct my_uart_port {
struct uart_port port;
struct dma_chan *dma_rx;
struct dma_chan *dma_tx;
struct dma_slave_config dma_rx_conf;
void *rx_buf;
dma_addr_t rx_dma_addr;
size_t rx_buf_size;
};
/* 1. DMA 채널 초기화 (probe 시) */
static int my_uart_dma_init(struct my_uart_port *up)
{
struct device *dev = up->port.dev;
struct dma_slave_config *conf = &up->dma_rx_conf;
/* Device Tree에서 DMA 채널 요청 (dmas = <&dma 0>) */
up->dma_rx = dma_request_chan(dev, "rx");
if (IS_ERR(up->dma_rx)) {
dev_warn(dev, "DMA RX channel not available\n");
up->dma_rx = NULL;
return -ENODEV;
}
/* RX DMA 버퍼 할당 (coherent) */
up->rx_buf_size = 4096;
up->rx_buf = dma_alloc_coherent(up->dma_rx->device->dev,
up->rx_buf_size, &up->rx_dma_addr, GFP_KERNEL);
if (!up->rx_buf) {
dma_release_channel(up->dma_rx);
return -ENOMEM;
}
/* Slave 설정 구성 */
memset(conf, 0, sizeof(*conf));
conf->direction = DMA_DEV_TO_MEM;
conf->src_addr = up->port.mapbase + UART_RX_REG; /* UART RX FIFO 물리 주소 */
conf->src_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
conf->src_maxburst = 1; /* FIFO 트리거 레벨 */
conf->device_fc = false; /* DMA 컨트롤러가 흐름 제어 */
return dmaengine_slave_config(up->dma_rx, conf);
}
/* 2. RX DMA 시작 */
static int my_uart_start_rx_dma(struct my_uart_port *up)
{
struct dma_async_tx_descriptor *desc;
/* cyclic DMA 디스크립터 생성 (링 버퍼) */
desc = dmaengine_prep_dma_cyclic(up->dma_rx,
up->rx_dma_addr,
up->rx_buf_size,
up->rx_buf_size / 4, /* period: 1KB마다 콜백 */
DMA_DEV_TO_MEM,
DMA_PREP_INTERRUPT);
if (!desc) {
dev_err(up->port.dev, "Failed to prep cyclic desc\n");
return -ENOMEM;
}
desc->callback = my_uart_dma_rx_complete;
desc->callback_param = up;
dmaengine_submit(desc);
dma_async_issue_pending(up->dma_rx);
return 0;
}
/* 3. RX 완료 콜백 (period마다 호출) */
static void my_uart_dma_rx_complete(void *param)
{
struct my_uart_port *up = param;
struct dma_tx_state state;
size_t count;
/* 현재 DMA 위치 조회 */
dmaengine_tx_status(up->dma_rx, 0, &state);
count = up->rx_buf_size - state.residue;
/* TTY 레이어로 데이터 전달 */
tty_insert_flip_string(&up->port.state->port,
up->rx_buf, count);
tty_flip_buffer_push(&up->port.state->port);
}
/* 4. DMA 중지 (shutdown 시) */
static void my_uart_stop_rx_dma(struct my_uart_port *up)
{
if (up->dma_rx) {
dmaengine_terminate_sync(up->dma_rx);
}
}
/* 5. 정리 (remove 시) */
static void my_uart_dma_cleanup(struct my_uart_port *up)
{
if (up->dma_rx) {
dmaengine_terminate_sync(up->dma_rx);
dma_free_coherent(up->dma_rx->device->dev,
up->rx_buf_size, up->rx_buf, up->rx_dma_addr);
dma_release_channel(up->dma_rx);
up->dma_rx = NULL;
}
}
dmatest 모듈 사용법
dmatest는 DMA Engine 프레임워크를 테스트하는 커널 모듈로, DMA 컨트롤러 드라이버 검증에 필수적입니다.
# 1. dmatest 모듈 로드 (CONFIG_DMATEST=m 필요)
modprobe dmatest
# 2. 테스트 파라미터 설정
# /sys/module/dmatest/parameters/ 디렉터리 사용
# 채널 지정 (예: dma0chan0)
echo dma0chan0 > /sys/module/dmatest/parameters/channel
# 전송 크기 (바이트)
echo 4096 > /sys/module/dmatest/parameters/test_buf_size
# 반복 횟수
echo 100 > /sys/module/dmatest/parameters/iterations
# 스레드 수
echo 1 > /sys/module/dmatest/parameters/threads_per_chan
# 타임아웃 (ms)
echo 5000 > /sys/module/dmatest/parameters/timeout
# 3. 테스트 시작
echo 1 > /sys/module/dmatest/parameters/run
# 4. 결과 확인
dmesg | grep dmatest
# 예상 출력:
# dmatest: Started 1 threads using dma0chan0
# dmatest: dma0chan0-copy0: summary 100 tests, 0 failures 1234 iops 12345 KB/s
# 5. 전체 DMA 컨트롤러 스트레스 테스트
# (채널 미지정 시 모든 채널 테스트)
echo "" > /sys/module/dmatest/parameters/channel
echo 1000 > /sys/module/dmatest/parameters/iterations
echo 1 > /sys/module/dmatest/parameters/run
# 6. 정지
echo 0 > /sys/module/dmatest/parameters/run
channel— 테스트할 채널 (비어있으면 모든 채널)device— 특정 DMA 디바이스 지정threads_per_chan— 채널당 동시 스레드 수max_channels— 최대 테스트 채널 수iterations— 반복 횟수 (0 = 무한)xor_sources— XOR 연산 소스 수 (async_tx 테스트)pq_sources— PQ 연산 소스 수 (RAID6 테스트)alignment— 버퍼 정렬 (바이트, 기본 1)
디버깅과 최적화
debugfs 인터페이스
DMA Engine은 /sys/kernel/debug/dmaengine/에 디버깅 인터페이스를 제공합니다 (CONFIG_DEBUG_FS 필요).
채널 요약 정보
# 모든 DMA 채널 상태 확인
cat /sys/kernel/debug/dmaengine/summary
# 출력 예시:
dma0 (pl330): number of channels: 8
dma0chan0 | in-use (1 refs) | spi-bcm2835.0:tx
dma0chan1 | in-use (1 refs) | spi-bcm2835.0:rx
dma0chan2 | in-use (1 refs) | uart-pl011.1:rx
dma0chan3 | idle
dma0chan4 | in-use (1 refs) | snd-soc-dummy:pcm-playback
dma0chan5 | in-use (1 refs) | snd-soc-dummy:pcm-capture
dma0chan6 | idle
dma0chan7 | idle
dma1 (dw-dma): number of channels: 4
dma1chan0 | idle
dma1chan1 | idle
dma1chan2 | in-use (1 refs) | i2c-designware.2:tx
dma1chan3 | in-use (1 refs) | i2c-designware.2:rx
개별 컨트롤러 상세 정보
# 특정 DMA 컨트롤러 정보
ls /sys/kernel/debug/dmaengine/
# dma0 dma1 summary
cat /sys/kernel/debug/dmaengine/dma0
# 출력 (컨트롤러별로 다름):
DMA Controller: pl330
Device: ff600000.dma
IRQ: 42, 43
Channels: 8
Capabilities: slave cyclic
# 실행 중인 전송 모니터링 (일부 드라이버만 지원)
watch -n 1 cat /sys/kernel/debug/dmaengine/summary
virt-dma 디버깅
# virt-dma 사용 드라이버의 디스크립터 상태
# (드라이버가 virt-dma 헬퍼를 사용하는 경우)
cat /sys/kernel/debug/dmaengine/dma0chan0/desc_list
# 출력 예시:
Submitted descriptors: 2
Issued descriptors: 1
Completed descriptors: 145
Allocated descriptors: 3
# 디스크립터 누수 감지
# Allocated - Completed 값이 계속 증가하면 누수 의심
ftrace로 DMA Engine 추적
ftrace는 DMA 전송의 실시간 흐름을 추적하는 강력한 도구입니다.
기본 DMA 이벤트 추적
# 1. DMA Engine 이벤트 활성화
cd /sys/kernel/debug/tracing
echo 1 > events/dma/enable
# 2. 함수 그래프 트레이서 설정 (선택)
echo function_graph > current_tracer
echo dmaengine_submit > set_graph_function
echo dma_async_issue_pending >> set_graph_function
# 3. 추적 시작
echo 1 > tracing_on
# 4. DMA 작업 수행 (예: SPI 전송)
# ... 애플리케이션 실행 ...
# 5. 추적 중지 및 결과 확인
echo 0 > tracing_on
cat trace
# 출력 예시:
# TASK-PID CPU# TIMESTAMP FUNCTION
spi-transfer-1234 [000] .... 12345.678901: dmaengine_submit <-spi_transfer_one_message
spi-transfer-1234 [000] .... 12345.678923: dma_async_issue_pending <-spi_transfer_one_message
dma-work-56 [001] d... 12345.679012: dma_cookie_complete <-pl330_tasklet
dma-work-56 [001] d... 12345.679034: dmaengine_desc_callback_invoke <-pl330_tasklet
특정 채널/디바이스만 추적
# 특정 DMA 컨트롤러 함수만 추적
echo 'pl330*' > /sys/kernel/debug/tracing/set_ftrace_filter
# 또는 glob 패턴 사용
echo 'dmaengine_*' > set_ftrace_filter
echo '*prep_slave*' >> set_ftrace_filter
# 필터 확인
cat set_ftrace_filter
주요 DMA ftrace 이벤트
| 이벤트 | 위치 | 설명 |
|---|---|---|
dmaengine:dma_prep | 디스크립터 준비 | 전송 준비 시작점 |
dmaengine:dma_issue | 전송 제출 | 하드웨어에 전송 전달 |
dmaengine:dma_complete | 전송 완료 | 인터럽트/폴링으로 완료 감지 |
dmaengine:dma_callback | 콜백 실행 | 완료 콜백 호출 |
DMA 지연 측정
# 전송 시작부터 완료까지 지연 측정
cd /sys/kernel/debug/tracing
# 타임스탬프 고정밀도 설정
echo 1 > options/funcgraph-abstime
echo 1 > options/latency-format
# DMA 이벤트만 추적
echo nop > current_tracer
echo 1 > events/dmaengine/enable
echo 1 > tracing_on
# ... DMA 작업 수행 ...
echo 0 > tracing_on
# 지연 분석
cat trace | grep -E 'dma_issue|dma_complete'
# 출력 예시:
# latency: 234 us, #2/2
spi-txrx-1234 12345.678901: dmaengine:dma_issue: chan=dma0chan0
irq/42-dma 12345.679135: dmaengine:dma_complete: chan=dma0chan0
# → 지연: 679135 - 678901 = 234us
히스토그램 분석 (커널 5.2+)
# DMA 완료 지연 히스토그램
echo 'hist:keys=common_pid:vals=lat:sort=lat' > \
/sys/kernel/debug/tracing/events/dmaengine/dma_complete/trigger
# 히스토그램 확인
cat /sys/kernel/debug/tracing/events/dmaengine/dma_complete/hist
# 출력 예시:
{ lat: 100 } hitcount: 12
{ lat: 150 } hitcount: 34
{ lat: 200 } hitcount: 56
{ lat: 250 } hitcount: 23
{ lat: 300 } hitcount: 8
일반적인 문제와 해결
| 문제 | 원인 | 해결 |
|---|---|---|
| 채널 할당 실패 | DT dmas 프로퍼티 누락/오류 | Device Tree 바인딩 확인, dma-names 일치 검증 |
| 전송 타임아웃 | HW 인터럽트 미발생, 클럭 미활성화 | 인터럽트 라인 확인, 클럭 게이팅 점검 |
| 데이터 불일치 | 캐시 동기화 누락 | dma_map_sg()/dma_unmap_sg() 정확히 쌍으로 호출 |
| DMA 매핑 오류 | dma_set_mask() 미호출 | Provider에서 적절한 DMA 마스크 설정 |
| 커널 패닉 | 완료 콜백에서 sleep | 콜백은 atomic context — sleep 불가, tasklet/workqueue 사용 |
| cyclic 콜백 지연 | period 크기가 너무 작음 | period 크기 증가, 인터럽트 코얼레싱 확인 |
성능 최적화 팁
- 배치 제출 — 여러 전송을
dmaengine_submit()으로 큐에 넣은 뒤dma_async_issue_pending()을 한 번만 호출하여 오버헤드를 줄입니다. - 적절한 버스트 크기 —
src_maxburst/dst_maxburst를 하드웨어 FIFO 크기에 맞춰 설정합니다. - 버스 폭 최적화 —
DMA_SLAVE_BUSWIDTH_4_BYTES이상 사용 시 전송 효율이 크게 향상됩니다. - DMA Pool — 디스크립터를 자주 할당/해제하면
dma_pool을 사용하여 단편화를 방지합니다. - scatter-gather 활용 — 물리적으로 불연속한 메모리도 단일 DMA 전송으로 처리하여 효율을 높입니다.
관련 Kconfig 옵션
| 옵션 | 기본값 | 설명 |
|---|---|---|
CONFIG_DMADEVICES | y | DMA Engine 서브시스템 활성화 |
CONFIG_DMA_ENGINE | y | DMA Engine 코어 |
CONFIG_DMA_VIRTUAL_CHANNELS | y | virt-dma 헬퍼 |
CONFIG_DMA_OF | y | Device Tree DMA 매핑 |
CONFIG_DMATEST | n | DMA 테스트 모듈 |
CONFIG_ASYNC_TX_DMA | n | async_tx DMA 가속 활성화 |
CONFIG_INTEL_IOATDMA | n | Intel IOAT DMA 드라이버 |
CONFIG_PL330_DMA | n | ARM PL330 DMA 드라이버 |
CONFIG_TI_EDMA | n | TI eDMA 드라이버 |
CONFIG_DW_DMAC | n | Synopsys DW-DMA 드라이버 |
관련 문서
DMA Engine과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.