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 컨트롤러 드라이버 품질 향상에 필요한 내용을 다룹니다.

관련 표준: DMA Engine API (kernel.org) — 커널 DMA 컨트롤러 추상화 계층 인터페이스입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
관련 문서: 커널 공식 DMA Engine 문서는 Documentation/driver-api/dmaengine/ 디렉터리에 있습니다. provider.rst(컨트롤러 드라이버), client.rst(클라이언트 API), dmatest.rst(테스트 모듈)를 참조하십시오.
교차 참조: DMA 매핑 API(Coherent/Streaming/SG/IOMMU)는 DMA 심화, 디바이스 모델 기초는 디바이스 드라이버, 오디오 DMA 활용은 ALSA, SPI/I2C DMA는 I2C/SPI/GPIO 페이지를 참조하십시오.
전제 조건: DMAIOMMU 문서를 먼저 읽으세요. DMA 경로는 CPU 우회 데이터 이동과 주소 변환 보호가 결합되므로, 매핑 수명주기와 동기화 지점을 먼저 고정해야 합니다.

핵심 요약

  • 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(스트라이드) 등을 지원합니다.

단계별 이해

  1. DMA Engine이 필요한 이유 — SoC에는 여러 DMA 컨트롤러가 존재하고, 각각 다른 레지스터/프로토콜을 사용합니다.

    DMA Engine 프레임워크는 이를 통일된 API로 추상화하여 클라이언트가 하드웨어 세부사항을 몰라도 DMA를 사용할 수 있게 합니다.

  2. 채널 요청dma_request_chan()으로 DMA 채널을 획득합니다. Device Tree의 dmas 프로퍼티로 매핑됩니다.
  3. 전송 설정dmaengine_slave_config()로 버스 폭, 버스트 크기 등을 설정합니다.
  4. 디스크립터 준비dmaengine_prep_slave_sg() 등으로 전송 디스크립터를 생성합니다.
  5. 전송 실행dmaengine_submit()으로 큐에 넣고, dma_async_issue_pending()으로 실제 전송을 시작합니다.

DMA Engine 개요

데이터 전송 방식은 PIO(Programmed I/O) → 인터럽트 구동 I/O → DMA → DMA Engine으로 발전해 왔습니다.

데이터 전송 발전 과정

단계방식특징한계
1PIOCPU가 직접 I/O 포트로 바이트 전송CPU 100% 점유, 느림
2인터럽트 구동디바이스 준비 시 인터럽트로 알림여전히 CPU가 데이터 복사
3DMA (하드웨어)DMA 컨트롤러가 CPU 대신 전송컨트롤러마다 다른 레지스터/프로토콜
4DMA Engine커널 프레임워크가 DMA 컨트롤러 추상화통일된 API, 다양한 전송 유형 지원

dmaengine 서브시스템의 역할

dmaengine 서브시스템은 다음을 제공합니다:

디렉터리 구조

/* 핵심 프레임워크 */
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 */

DMA 컨트롤러 하드웨어 내부 구조

SoC DMA 컨트롤러는 일반적으로 다음 구성 요소를 포함합니다. 컨트롤러마다 세부 구현은 다르지만 기본 구조는 동일합니다.

DMA 컨트롤러 하드웨어 내부 구조 AHB/AXI Master 메모리 버스 인터페이스 채널 블록 (Channel 0~N) SRC Addr 레지스터 DST Addr 레지스터 Control/Size 레지스터 FIFO 버퍼 8~32 word depth LLI 포인터 Linked-List Item 중재기 (Arbiter) Round-Robin / 우선순위 DMAMUX (Request MUX) 페리퍼럴 → 채널 매핑 SPI_TX→CH0, UART_RX→CH1, ... 페리퍼럴 요청 (DREQ) SPI, UART, I2C, I2S, ADC 하드웨어 핸드셰이크 신호 인터럽트 컨트롤러 TC (Transfer Complete) ERR, Half-TC, Abort Linked-List Item (LLI) 메모리 — Scatter-Gather 디스크립터 체인 LLI 0: src,dst,len LLI 1: src,dst,len LLI 2: src,dst,len LLI N: (last) → NULL 또는 LLI 0 (cyclic)
구성 요소역할예시
채널 (Channel)독립적인 데이터 전송 경로PL330: 8채널, STM32 DMA2: 8채널
FIFO소스/목적지 속도 차이 흡수4~32 word, 버스트 전송 단위
중재기 (Arbiter)여러 채널의 버스 접근 우선순위 결정Round-Robin, Fixed Priority
DMAMUX페리퍼럴 요청을 특정 채널에 연결STM32 DMAMUX, NXP EDMA MUX
LLI (Linked-List Item)메모리에 저장된 전송 디스크립터 체인SG 전송, Cyclic 전송 구현
핸드셰이크 (DREQ/DACK)페리퍼럴과 DMA 간 흐름 제어 신호FIFO 준비 → DREQ 발생 → DMA 전송

하드웨어 핸드셰이크 (DREQ/DACK)

Slave DMA에서 데이터 전송 속도를 페리퍼럴의 FIFO 상태에 맞추기 위해 하드웨어 핸드셰이크 신호를 사용합니다.

Slave DMA 핸드셰이크: UART RX → 메모리 UART FIFO DREQ DMA 전송 메모리 채움 HIGH burst burst burst DMA 수신 버퍼 (연속 채움) ① FIFO가 threshold 도달 ② DREQ 신호 활성화 ③ DMA burst 전송 실행 ④ FIFO 비워짐 → DREQ 해제 ⑤ 다음 threshold까지 대기
설정dma_slave_config 필드의미
버스트 크기src_maxburst / dst_maxburst한 번의 DREQ에 전송할 데이터 단위 수 (FIFO depth의 절반이 일반적)
버스 폭src_addr_width / dst_addr_width1/2/4/8/16/32/64 바이트 (FIFO 레지스터 폭에 맞춤)
흐름 제어device_fctrue: 페리퍼럴이 전송 길이 제어, false: DMA 컨트롤러가 제어
FIFO 주소src_addr / dst_addr페리퍼럴 FIFO의 물리 주소 (가상 주소 아님!)
흔한 실수: src_addr/dst_addrioremap()으로 얻은 가상 주소를 전달하면 안 됩니다. DMA 컨트롤러는 CPU MMU를 거치지 않으므로 반드시 물리 주소를 사용해야 합니다. 플랫폼 디바이스에서는 platform_get_resource()로 물리 주소를 얻으십시오.

흐름 제어 (Flow Controller)

DMA 전송의 길이를 누가 결정하느냐에 따라 흐름 제어 모드가 나뉩니다:

모드device_fc전송 길이 결정사용 사례
DMA-controlledfalse (기본)DMA 컨트롤러가 카운터 관리SPI, I2C, UART (길이 사전 알려짐)
Peripheral-controlledtrue페리퍼럴이 TC 신호로 종료 통보SD/MMC (블록 크기가 가변), NAND

아키텍처

Provider vs Consumer 모델

DMA Engine은 Provider-Consumer 패턴을 사용합니다:

Consumer (클라이언트 드라이버) SPI 드라이버 UART 드라이버 ALSA (오디오) async_tx 네트워크 dma_request_chan() / dmaengine_prep_*() / dmaengine_submit() DMA Engine Core (dmaengine.c) dma_async_device_register() / callbacks Provider (DMA 컨트롤러 드라이버) PL330 드라이버 DW-DMA 드라이버 IOAT 드라이버 eDMA 드라이버 DMA 컨트롤러 하드웨어 (PL330, IOAT, eDMA, DW-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;                           /* 디바이스 흐름 제어 */
    ...
};

virt-dma 헬퍼 프레임워크

drivers/dma/virt-dma.hvirt-dma 헬퍼는 대부분의 DMA 컨트롤러 드라이버가 사용하는 디스크립터 관리 프레임워크입니다. 가상 채널(struct virt_dma_chan)과 가상 디스크립터(struct virt_dma_desc)를 통해 제출/발행/완료 큐 관리, tasklet 기반 콜백 처리를 자동화합니다.

virt-dma 디스크립터 라이프사이클 할당됨 prep_*() 반환 제출됨 desc_submitted 큐 발행됨 desc_issued 큐 실행 중 HW 전송 완료 desc_completed submit issue HW start IRQ vchan_tx_submit() issue_pending() HW 프로그래밍 vchan_cookie_complete() vchan_complete tasklet 완료된 디스크립터 콜백 호출 + 해제 desc_free 콜백 → 드라이버별 정리 (DMA 디스크립터 메모리 해제)
/* virt-dma 핵심 구조체 */
struct virt_dma_chan {
    struct dma_chan chan;
    struct tasklet_struct task;   /* 완료 콜백 실행용 tasklet */
    struct list_head desc_submitted; /* submit() 후 대기 큐 */
    struct list_head desc_issued;    /* issue_pending() 후 큐 */
    struct list_head desc_completed;  /* IRQ로 완료된 큐 */
    struct list_head desc_allocated;  /* 할당만 된 큐 */
    void (*desc_free)(struct virt_dma_desc *);
};

struct virt_dma_desc {
    struct dma_async_tx_descriptor tx;
    struct list_head node;
};

/* Provider 드라이버에서의 사용 */
/* 1. 채널 초기화 시 vchan_init() 호출 */
vchan_init(&mychan->vc, dma_dev);
mychan->vc.desc_free = my_desc_free;

/* 2. prep_*() 콜백에서 디스크립터 생성 */
struct my_desc *d = kzalloc(sizeof(*d), GFP_NOWAIT);
return vchan_tx_prep(&mychan->vc, &d->vd, flags);

/* 3. issue_pending() 콜백에서 submitted → issued 이동 */
vchan_issue_pending(&mychan->vc);
/* → 내부에서 desc_submitted → desc_issued 큐 이동 */

/* 4. IRQ 핸들러에서 완료 통보 */
vchan_cookie_complete(&mydesc->vd);
/* → desc_issued → desc_completed 이동 + tasklet 스케줄 */
virt-dma 장점: 디스크립터 큐 관리, 쿠키 추적, 콜백 호출, terminate_all() 구현을 자동화합니다. 신규 DMA 컨트롤러 드라이버에서는 반드시 virt-dma 헬퍼를 사용하십시오. 커널 소스의 90% 이상의 DMA 드라이버가 이를 활용합니다.

Provider API (컨트롤러 드라이버)

DMA 컨트롤러 드라이버(Provider)는 struct dma_device를 채운 뒤 dma_async_device_register()로 프레임워크에 등록합니다.

필수 콜백 구현

콜백역할필수 여부
device_alloc_chan_resources채널 리소스 할당 (디스크립터 풀 등)필수
device_free_chan_resources채널 리소스 해제필수
device_prep_dma_memcpymemcpy 전송 디스크립터 생성DMA_MEMCPY 지원 시
device_prep_slave_sgslave scatter-gather 디스크립터 생성DMA_SLAVE 지원 시
device_prep_dma_cycliccyclic 전송 디스크립터 생성DMA_CYCLIC 지원 시
device_prep_interleaved_dmainterleaved 전송 디스크립터 생성DMA_INTERLEAVE 지원 시
device_issue_pending보류 중인 전송을 하드웨어에 전달필수
device_tx_status전송 상태 조회 (완료/진행 중)필수
device_terminate_all진행 중인 모든 전송 취소권장
device_configslave 설정 적용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);
}
virt-dma 헬퍼: 대부분의 Provider 드라이버는 drivers/dma/virt-dma.hvchan_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);

DMAMUX (DMA Request Multiplexer)

최신 SoC에서는 주변장치 수가 DMA 채널 수보다 훨씬 많습니다. DMAMUX는 N개의 DMA 요청 소스를 M개의 DMA 채널에 동적으로 라우팅하는 하드웨어 멀티플렉서입니다.

DMAMUX — 요청 라우팅 아키텍처 SPI0 TX (req 0) SPI0 RX (req 1) UART0 TX (req 2) UART0 RX (req 3) I2C0 (req 4) ADC (req 5) ... (req N) DMAMUX CH0: req_sel = 0 (SPI TX) CH1: req_sel = 1 (SPI RX) CH2: req_sel = 3 (UART RX) CH3: req_sel = 5 (ADC) CH4~7: disabled DMA Controller Channel 0 → SPI TX Channel 1 → SPI RX Channel 2 → UART RX Channel 3 → ADC

DMAMUX Device Tree 바인딩

DMAMUX가 있는 SoC에서는 #dma-cells 값이 보통 2 이상으로, 첫 번째 셀이 DMAMUX 요청 번호를 나타냅니다.

/* STM32 DMAMUX Device Tree 예시 */
dmamux1: dma-router@40020800 {
    compatible = "st,stm32h7-dmamux";
    reg = <0x40020800 0x1c>;
    #dma-cells = <3>;
    dma-requests = <107>; /* 107개 주변장치 요청 */
    dma-channels = <16>; /* 16개 DMA 채널 */
    dma-masters = <&dma1 &dma2>; /* 2개의 DMA 컨트롤러 */
    clocks = <&rcc DMA1_CK>;
};

/* 클라이언트에서 DMAMUX 경유 채널 요청 */
spi1: spi@40013000 {
    compatible = "st,stm32h7-spi";
    /* dmamux_req_id, channel_cfg, fifo_cfg */
    dmas = <&dmamux1 38 0x400 0x05>,
           <&dmamux1 37 0x400 0x05>;
    dma-names = "tx", "rx";
};

DMAMUX 드라이버 구조

커널에서 DMAMUX는 struct dma_router로 표현되며, of_dma_router_xlate() 콜백을 통해 요청 번호를 실제 DMA 채널에 매핑합니다.

struct dma_router {
    struct device *dev;
    void (*route_free)(struct device *dev, void *route_data);
};

/* DMAMUX xlate 콜백 — of_dma_controller_register()로 등록 */
static struct dma_chan *stm32_dmamux_xlate(
    struct of_phandle_args *dma_spec,
    struct of_dma *ofdma)
{
    struct stm32_dmamux *dmamux = ofdma->of_dma_data;
    u32 request = dma_spec->args[0]; /* DMAMUX 요청 번호 */

    /* 사용 가능한 DMA 채널 탐색 */
    chan = __dma_request_channel(&mask, stm32_dmamux_filter, dmamux, ofdma->of_node);

    /* DMAMUX 레지스터에 요청 번호 설정 */
    writel(request, dmamux->base + DMAMUX_CxCR(chan_id));

    return chan;
}

플랫폼별 DMAMUX 구현

플랫폼DMAMUX 드라이버요청 수채널 수특징
STM32H7/MP1dma/stm32-dmamux.c10716 (DMA1+DMA2)동기 생성기, 요청 동기화
NXP i.MX8dma/fsl-edma.c 내장64+32DMAMUX 레지스터 직접 제어
Renesas RZ/G2dma/sh/rcar-dmac.cSoC별15MID-RID 기반 매핑
TI AM62xdma/ti/k3-psil.cPSIL threadBCDMA/PKTDMAPSI-L fabric 라우팅
DMAMUX vs 고정 매핑: PL330, DW-DMA 등 구세대 컨트롤러는 채널-주변장치 매핑이 하드웨어에 고정됩니다. DMAMUX가 있으면 런타임에 임의의 요청을 임의의 채널에 연결할 수 있어 채널 활용률이 높아집니다.

채널 필터링과 xlate

DMA Engine 프레임워크는 Device Tree/ACPI에서 DMA 채널을 자동으로 매핑합니다. 이 과정의 핵심은 of_dma_xlate 콜백입니다.

xlate 처리 흐름

dma_request_chan() → xlate → 채널 할당 흐름 클라이언트 드라이버 dma_request_chan() of_dma_request_slave_channel DT dmas 파싱 of_dma_xlate() 채널 선택 + 설정 dma_chan 할당 완료 alloc_chan _resources ① DT 파싱: dmas = <&dma_ctrl req_id cfg>; → of_phandle_args {np, args[], args_count} ② xlate 호출: Provider가 등록한 xlate 콜백이 args를 해석하여 적절한 채널 반환 ③ 필터 함수: dma_filter_fn으로 채널 적합성 검증 (ACPI 경로) / xlate가 직접 선택 (DT 경로)

xlate 유형

xlate 함수#dma-cells용도사용 컨트롤러
of_dma_xlate_by_chan_id1셀 값 = 채널 번호PL330, DW-DMA
커스텀 xlate2~4요청 번호 + 설정 인코딩STM32 DMA, TI eDMA, K3 UDMA
of_dma_simple_xlate1단순 채널 매핑범용

커스텀 xlate 구현 예시

/* #dma-cells = <2>: cell[0]=request_id, cell[1]=config */
static struct dma_chan *my_dma_xlate(
    struct of_phandle_args *dma_spec,
    struct of_dma *ofdma)
{
    struct my_dma_dev *mydev = ofdma->of_dma_data;
    u32 request_id = dma_spec->args[0];
    u32 config = dma_spec->args[1];
    struct dma_chan *chan;

    if (request_id >= mydev->nr_requests)
        return ERR_PTR(-EINVAL);

    /* 사용 가능한 채널 탐색 */
    chan = dma_get_any_slave_channel(&mydev->dma_dev);
    if (!chan)
        return ERR_PTR(-EBUSY);

    /* 채널에 요청 ID와 설정 저장 */
    struct my_dma_chan *mchan = to_my_chan(chan);
    mchan->request_id = request_id;
    mchan->config = config;

    return chan;
}

/* probe에서 등록 */
of_dma_controller_register(np, my_dma_xlate, mydev);

ACPI DMA 채널 매핑

ACPI 환경(x86 서버 등)에서는 Device Tree 대신 acpi_dma_controller_register()와 필터 함수를 사용합니다.

/* ACPI DMA 컨트롤러 등록 */
acpi_dma_controller_register(dev, my_acpi_dma_xlate, mydev);

/* ACPI 클라이언트에서 채널 요청 */
/* _DMA ACPI 리소스를 통해 자동 매핑됨 */
chan = dma_request_chan(dev, "tx");
/* ACPI에서는 _DSD 프로퍼티로 채널 이름 매핑:
 * Name (_DSD, Package () {
 *   ToUUID("daffd814-..."),
 *   Package () {
 *     Package () { "tx", Package () { ^DMA, 0 } }
 *   }
 * })
 */
주의: dma_request_slave_channel()은 레거시 API로, 에러 코드를 반환하지 않고 NULL을 반환하여 디버깅이 어렵습니다. 항상 dma_request_chan()을 사용하고 IS_ERR()로 확인하십시오.

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 CopyDMA_MEMCPY메모리 → 메모리 복사Intel IOAT memcpy offload, 대용량 메모리 복사
Memory SetDMA_MEMSET메모리를 특정 값으로 채움버퍼 초기화
SlaveDMA_SLAVE디바이스 ↔ 메모리 (scatter-gather)SPI, I2C, UART 데이터 전송
CyclicDMA_CYCLIC순환 버퍼 전송 (링 버퍼)오디오 PCM 재생/녹음, ADC 연속 샘플링
InterleavedDMA_INTERLEAVE스트라이드 패턴 전송2D DMA, 프레임버퍼 전송
XORDMA_XORXOR 연산 (RAID5 패리티)async_tx RAID5 가속
PQDMA_PQP+Q 연산 (RAID6)async_tx RAID6 가속

전송 방향

방향매크로설명
메모리 → 메모리DMA_MEM_TO_MEMmemcpy, memset 전송
메모리 → 디바이스DMA_MEM_TO_DEVTX: RAM에서 디바이스 FIFO로
디바이스 → 메모리DMA_DEV_TO_MEMRX: 디바이스 FIFO에서 RAM으로
디바이스 → 디바이스DMA_DEV_TO_DEV디바이스 간 직접 전송 (드문 경우)

전송 플래그

플래그설명
DMA_PREP_INTERRUPT전송 완료 시 인터럽트/콜백 발생
DMA_CTRL_ACK프레임워크가 디스크립터를 재사용 가능하게 표시
DMA_PREP_CMD명령 DMA 전송 (특수 하드웨어)
DMA_PREP_REPEAT반복 전송 (cyclic과 함께 사용)

Interleaved DMA (2D/3D 전송)

Interleaved DMA는 비연속 메모리 패턴(스트라이드, 2D 블록 전송)을 단일 DMA 연산으로 처리합니다. 프레임버퍼 전송, 이미지 로테이션, 행렬 전치 등에서 CPU 개입 없이 복잡한 메모리 접근 패턴을 실현합니다.

Interleaved DMA — 2D 블록 전송 소스 메모리 (src_stride=1024) chunk[0] gap (icg) chunk[1] chunk[2] chunk[3] sgl[0].size DMA 목적지 (dst_stride=640) chunk[0] chunk[1] chunk[2] chunk[3] dma_interleaved_template 구조: src_start = 소스 시작 주소 | dst_start = 목적지 시작 주소 src_inc = true, dst_inc = true | src_sgl = true, dst_sgl = true numf = 행(frame) 수 | frame_size = 1 | sgl[0] = {.size=chunk_size, .icg=gap_size}

dma_interleaved_template 구조체

struct dma_interleaved_template {
    dma_addr_t src_start;     /* 소스 시작 주소 */
    dma_addr_t dst_start;     /* 목적지 시작 주소 */
    enum dma_transfer_direction dir;
    bool src_inc;              /* 소스 주소 증가 여부 */
    bool dst_inc;              /* 목적지 주소 증가 여부 */
    bool src_sgl;              /* 소스에 scatter-gather 간격 적용 */
    bool dst_sgl;              /* 목적지에 scatter-gather 간격 적용 */
    size_t numf;               /* 프레임(행) 수 */
    size_t frame_size;         /* 프레임당 chunk 수 */
    struct data_chunk sgl[];   /* chunk 배열 */
};

struct data_chunk {
    size_t size;               /* 전송할 바이트 수 */
    size_t icg;                /* Inter-Chunk Gap (바이트) */
    size_t dst_icg;            /* 목적지 ICG (별도 지정 시) */
    size_t src_icg;            /* 소스 ICG (별도 지정 시) */
};

Interleaved DMA 사용 예시

2D 블록 복사 (프레임버퍼 영역 전송)

/* 1024x768 프레임버퍼에서 100x50 영역을 복사 */
struct dma_interleaved_template *xt;
size_t src_stride = 1024 * 4;  /* 소스 행 바이트 (RGBX) */
size_t dst_stride = 100 * 4;   /* 목적지 행 바이트 */
size_t chunk = 100 * 4;        /* 한 행에서 복사할 바이트 */

xt = kzalloc(sizeof(*xt) + sizeof(struct data_chunk), GFP_KERNEL);
xt->src_start = src_dma_addr;
xt->dst_start = dst_dma_addr;
xt->dir = DMA_MEM_TO_MEM;
xt->src_inc = true;
xt->dst_inc = true;
xt->src_sgl = true;
xt->dst_sgl = true;
xt->numf = 50;               /* 50행 */
xt->frame_size = 1;           /* 프레임당 1 chunk */
xt->sgl[0].size = chunk;      /* 400 바이트/행 */
xt->sgl[0].icg = src_stride - chunk; /* 소스 갭 */
xt->sgl[0].dst_icg = dst_stride - chunk; /* 목적지 갭 */

desc = dmaengine_prep_interleaved_dma(chan, xt, DMA_PREP_INTERRUPT);
if (desc) {
    desc->callback = blit_done;
    dmaengine_submit(desc);
    dma_async_issue_pending(chan);
}
kfree(xt);

주변장치 → 메모리 스트라이드 전송

/* ADC 4채널 인터리브드 샘플링 → 채널별 분리 저장 */
/* ADC 출력: [ch0,ch1,ch2,ch3, ch0,ch1,ch2,ch3, ...] */
/* 목적지: ch0 버퍼에 ch0 샘플만 연속 저장 */
xt->src_start = adc_fifo_addr;
xt->dst_start = ch0_buf_dma;
xt->dir = DMA_DEV_TO_MEM;
xt->src_inc = false;       /* FIFO: 주소 고정 */
xt->dst_inc = true;
xt->src_sgl = false;
xt->dst_sgl = false;
xt->numf = num_samples;
xt->frame_size = 1;
xt->sgl[0].size = 2;     /* 16비트 샘플 */
xt->sgl[0].icg = 6;      /* 나머지 3채널 건너뜀 (3×2=6) */
지원 여부 확인: 모든 DMA 컨트롤러가 interleaved 전송을 지원하지는 않습니다. dma_cap_set(DMA_INTERLEAVE, ...) 확인 후 사용하세요. 지원하지 않는 경우 다중 slave_sg 전송으로 분해해야 합니다.

Metadata 모드

일부 DMA 컨트롤러는 데이터 전송과 함께 메타데이터를 전달하는 특수 모드를 지원합니다. 커널 5.0+에서 도입된 metadata API는 디스크립터에 클라이언트별 메타데이터를 첨부합니다.

메타데이터 연산

연산함수설명
메타데이터 설정dmaengine_desc_attach_metadata()디스크립터에 메타데이터 버퍼 첨부
메타데이터 가져오기dmaengine_desc_get_metadata_ptr()Provider가 관리하는 메타데이터 포인터 획득
메타데이터 설정dmaengine_desc_set_metadata_len()메타데이터 길이 설정

사용 예시 (TI K3 UDMA)

/* K3 UDMA: 패킷 DMA에 프로토콜별 헤더(PSDATA) 첨부 */
struct dma_async_tx_descriptor *desc;
void *md_ptr;
size_t md_len;

desc = dmaengine_prep_slave_sg(chan, sg, nents,
    DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT | DMA_CTRL_ACK);

/* 방법 1: 클라이언트가 메타데이터 제공 */
u8 metadata[16];
fill_protocol_header(metadata);
dmaengine_desc_attach_metadata(desc, metadata, 16);

/* 방법 2: Provider 메타데이터 버퍼 사용 */
md_ptr = dmaengine_desc_get_metadata_ptr(desc, &md_len, &max_len);
memcpy(md_ptr, my_header, my_header_len);
dmaengine_desc_set_metadata_len(desc, my_header_len);

dmaengine_submit(desc);
DMA_PREP_CMD: DMA_PREP_CMD 플래그는 데이터가 아닌 명령을 DMA로 전송합니다. MediaTek의 UART DMA 등에서 사용되며, 하드웨어가 명령 디스크립터를 별도로 해석합니다.

Residue 보고

DMA 전송 중 또는 완료 후, 아직 전송되지 않은 잔여 바이트(residue)를 조회하는 것은 UART RX, 오디오 PCM 위치 추적 등에서 핵심적입니다.

Residue 조회 API

struct dma_tx_state {
    dma_cookie_t last;       /* 마지막 완료 쿠키 */
    dma_cookie_t used;       /* 마지막 사용(제출) 쿠키 */
    u32 residue;              /* 잔여 바이트 수 */
    struct dma_slave_caps caps; /* (커널 6.x) */
};

enum dma_status dmaengine_tx_status(
    struct dma_chan *chan,
    dma_cookie_t cookie,
    struct dma_tx_state *state);

Residue 정밀도

정밀도매크로설명지원 컨트롤러
디스크립터DMA_RESIDUE_GRANULARITY_DESCRIPTOR디스크립터 단위 (SG 엔트리)대부분의 기본 컨트롤러
세그먼트DMA_RESIDUE_GRANULARITY_SEGMENTSG 세그먼트 단위eDMA, K3 UDMA
버스트DMA_RESIDUE_GRANULARITY_BURST버스트 전송 단위PL330, STM32 DMA

Provider에서 Residue 구현

static enum dma_status my_tx_status(
    struct dma_chan *chan,
    dma_cookie_t cookie,
    struct dma_tx_state *txstate)
{
    struct my_dma_chan *mchan = to_my_chan(chan);
    enum dma_status ret;
    unsigned long flags;
    u32 residue;

    ret = dma_cookie_status(chan, cookie, txstate);
    if (ret == DMA_COMPLETE || !txstate)
        return ret;

    spin_lock_irqsave(&mchan->vchan.lock, flags);

    /* 하드웨어 레지스터에서 현재 전송 위치 읽기 */
    if (mchan->desc && mchan->desc->vd.tx.cookie == cookie) {
        u32 pos = readl(mchan->base + DMA_CURR_ADDR);
        residue = mchan->desc->total_len -
                  (pos - mchan->desc->start_addr);
    } else {
        /* 큐에 대기 중인 디스크립터: 전체 길이가 residue */
        struct virt_dma_desc *vd;
        vd = vchan_find_desc(&mchan->vchan, cookie);
        residue = vd ? to_my_desc(vd)->total_len : 0;
    }

    dma_set_residue(txstate, residue);
    spin_unlock_irqrestore(&mchan->vchan.lock, flags);

    return ret;
}

Consumer에서 Residue 활용 (UART)

/* UART RX: 실제 수신된 바이트 수 계산 */
struct dma_tx_state state;
size_t received;

dmaengine_tx_status(rx_chan, cookie, &state);
received = total_buf_size - state.residue;

/* PCM 재생 위치 (ALSA) */
static snd_pcm_uframes_t my_pcm_pointer(
    struct snd_pcm_substream *ss)
{
    struct dma_tx_state state;
    dmaengine_tx_status(chan, cookie, &state);
    return bytes_to_frames(ss->runtime,
        ss->runtime->dma_bytes - state.residue);
}

에러 처리와 복구

DMA 전송은 버스 에러, 타임아웃, FIFO 오버런 등 다양한 이유로 실패할 수 있습니다. 견고한 드라이버는 이러한 상황을 감지하고 복구해야 합니다.

에러 감지 메커니즘

에러 유형원인감지 방법복구 절차
버스 에러잘못된 DMA 주소, IOMMU 위반DMA_ERROR 상태매핑 확인, 채널 재설정
전송 타임아웃주변장치 무응답, 클럭 정지wait_for_completion_timeout()terminate + 재초기화
FIFO 오버런DMA가 데이터를 충분히 빨리 소비하지 못함주변장치 상태 레지스터버스트 크기 조정
설정 에러지원하지 않는 버스 폭/방향dmaengine_slave_config() 반환값설정 수정
디스크립터 부족디스크립터 풀 소진dmaengine_prep_*() = NULL완료 대기 후 재시도

terminate API 세부사항

/* 비동기 종료: 콜백 중에도 안전하게 호출 가능 */
int dmaengine_terminate_async(struct dma_chan *chan);
/* → 즉시 반환, 실행 중인 콜백은 완료될 때까지 대기하지 않음 */

/* 동기 종료: 모든 콜백 완료까지 대기 */
void dmaengine_terminate_sync(struct dma_chan *chan);
/* → 콜백 내에서 호출하면 데드락! */

/* 동기화만 수행 (terminate 없이) */
void dmaengine_synchronize(struct dma_chan *chan);
/* → terminate_async() 호출 후, 나중에 동기화할 때 사용 */

일시정지 / 재개

/* 전송 일시정지 — 하드웨어가 지원할 때만 */
int dmaengine_pause(struct dma_chan *chan);

/* 전송 재개 */
int dmaengine_resume(struct dma_chan *chan);

/* 일시정지 후 residue 조회 */
dmaengine_pause(chan);
dmaengine_tx_status(chan, cookie, &state);
/* state.residue 에서 정확한 전송 위치 확인 */

/* 필요시 재개 또는 종료 */
if (error_detected)
    dmaengine_terminate_async(chan);
else
    dmaengine_resume(chan);

에러 복구 패턴

static void my_dma_error_handler(struct work_struct *work)
{
    struct my_device *dev = container_of(work,
        struct my_device, error_work);

    /* 1. 진행 중인 모든 DMA 중지 */
    dmaengine_terminate_sync(dev->dma_chan);

    /* 2. 하드웨어 상태 초기화 */
    my_hw_reset(dev);

    /* 3. DMA 채널 재설정 */
    dmaengine_slave_config(dev->dma_chan, &dev->dma_cfg);

    /* 4. 전송 재시작 */
    my_start_dma_transfer(dev);

    dev_info(dev->dev, "DMA error recovery complete\n");
}

/* 콜백에서 에러 감지 시 워크큐로 복구 위임 */
static void my_dma_callback(void *param)
{
    struct my_device *dev = param;
    struct dma_tx_state state;
    enum dma_status status;

    status = dmaengine_tx_status(dev->dma_chan,
        dev->cookie, &state);
    if (status == DMA_ERROR) {
        dev_err(dev->dev, "DMA error, residue=%u\n",
            state.residue);
        schedule_work(&dev->error_work);
        return;
    }
    /* 정상 완료 처리 */
    complete(&dev->dma_done);
}
데드락 주의: dmaengine_terminate_sync()는 실행 중인 콜백이 완료될 때까지 대기합니다. 따라서 콜백 내에서 호출하면 자기 자신을 대기하게 되어 데드락이 발생합니다. 콜백에서는 반드시 dmaengine_terminate_async()를 사용하고, 워크큐에서 dmaengine_synchronize()로 동기화하십시오.

전원 관리

DMA 컨트롤러의 전원 관리는 배터리 구동 임베디드 시스템에서 중요합니다. Runtime PM을 활용하여 유휴 채널의 클럭을 차단하고, 시스템 서스펜드 시 진행 중인 전송을 안전하게 중단/복구합니다.

Runtime PM 통합

/* Provider: probe 시 Runtime PM 설정 */
static int my_dma_probe(struct platform_device *pdev)
{
    /* ... DMA 컨트롤러 초기화 ... */

    pm_runtime_set_autosuspend_delay(&pdev->dev, 500); /* 500ms */
    pm_runtime_use_autosuspend(&pdev->dev);
    pm_runtime_set_active(&pdev->dev);
    pm_runtime_enable(&pdev->dev);

    return dma_async_device_register(dma_dev);
}

/* Runtime suspend: 클럭 차단 */
static int my_dma_runtime_suspend(struct device *dev)
{
    struct my_dma_dev *mydev = dev_get_drvdata(dev);
    clk_disable_unprepare(mydev->clk);
    return 0;
}

/* Runtime resume: 클럭 복원 */
static int my_dma_runtime_resume(struct device *dev)
{
    struct my_dma_dev *mydev = dev_get_drvdata(dev);
    return clk_prepare_enable(mydev->clk);
}

/* 채널 리소스 할당/해제 시 PM 참조 카운트 관리 */
static int my_alloc_chan_resources(struct dma_chan *chan)
{
    pm_runtime_get_sync(chan->device->dev);
    /* ... 디스크립터 풀 할당 ... */
    return 0;
}

static void my_free_chan_resources(struct dma_chan *chan)
{
    /* ... 디스크립터 풀 해제 ... */
    pm_runtime_mark_last_busy(chan->device->dev);
    pm_runtime_put_autosuspend(chan->device->dev);
}

시스템 서스펜드 처리

static int my_dma_suspend(struct device *dev)
{
    struct my_dma_dev *mydev = dev_get_drvdata(dev);

    /* 모든 활성 채널의 전송 중지 */
    for (int i = 0; i < mydev->nr_channels; i++) {
        struct my_dma_chan *mchan = &mydev->chans[i];
        if (mchan->busy) {
            /* 하드웨어 레지스터 상태 저장 */
            my_save_channel_state(mchan);
            /* 전송 일시정지 */
            my_pause_channel(mchan);
        }
    }

    /* 글로벌 레지스터 저장 */
    my_save_global_regs(mydev);

    clk_disable_unprepare(mydev->clk);
    return 0;
}

static int my_dma_resume(struct device *dev)
{
    struct my_dma_dev *mydev = dev_get_drvdata(dev);

    clk_prepare_enable(mydev->clk);

    /* 글로벌 레지스터 복원 */
    my_restore_global_regs(mydev);

    /* 활성 채널 복원 */
    for (int i = 0; i < mydev->nr_channels; i++) {
        struct my_dma_chan *mchan = &mydev->chans[i];
        if (mchan->busy) {
            my_restore_channel_state(mchan);
            my_resume_channel(mchan);
        }
    }
    return 0;
}

static const struct dev_pm_ops my_dma_pm_ops = {
    SET_SYSTEM_SLEEP_PM_OPS(my_dma_suspend, my_dma_resume)
    SET_RUNTIME_PM_OPS(my_dma_runtime_suspend,
        my_dma_runtime_resume, NULL)
};
No-IRQ PM: DMA 컨트롤러가 인터럽트 의존성이 있는 경우, SET_NOIRQ_SYSTEM_SLEEP_PM_OPS()를 사용하여 인터럽트 비활성화 후(late suspend)에 서스펜드 처리하면 안전합니다.

async_tx API

async_tx는 DMA Engine 위에 구축된 비동기 메모리 연산 프레임워크로, 주로 소프트웨어 RAID(md)에서 XOR/PQ 패리티 계산을 가속합니다.

주요 함수

함수연산용도
async_memcpy()메모리 복사DMA 가속 memcpy
async_xor()XORRAID5 패리티 계산
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)x86drivers/dma/ioat/memcpy offload, DCA (Direct Cache Access), 서버급
ARM PL330 (DMA-330)ARM SoC (Samsung, etc.)drivers/dma/pl330.c8채널, 마이크로 코드 기반, Exynos/RK3399 등
TI eDMA3TI AM335x/AM57xdrivers/dma/ti/edma.c이벤트 트리거, 64채널, PaRAM 디스크립터
TI K3 UDMATI AM62x/AM64xdrivers/dma/ti/k3-udma.c패킷 기반 DMA, NAVSS, 링 가속기
NXP eDMAi.MX8/LPCdrivers/dma/fsl-edma.c최대 64채널, 주변장치 전용
STM32 DMASTM32 MCUdrivers/dma/stm32-dma.cRequest Mux (DMAMUX), FIFO, 이중 버퍼
Synopsys DW-DMA다양한 SoCdrivers/dma/dw/Multi-master, 블록 체인, Intel Baytrail 등
Qualcomm BAMSnapdragondrivers/dma/qcom/bam_dma.cPipe 기반, 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>;
};

PL330 마이크로코드 아키텍처

ARM PL330 (DMA-330)은 독특하게 마이크로 프로그래밍 방식으로 동작합니다. DMA 전송이 하드웨어 디스크립터 체인 대신 마이크로 코드 명령어로 제어됩니다.

PL330 DMA-330 마이크로코드 실행 모델 Manager Thread DMAGO → 채널 시작 DMASEV → 이벤트 발생 DMAKILL → 채널 중지 DMAWFE → 이벤트 대기 Channel Thread (×8) DMALD → FIFO로 읽기 DMAST → FIFO에서 쓰기 DMALP/DMALPEND → 루프 DMAWFP → 주변장치 대기 MFIFO 데이터 버퍼 (구성 가능) 마이크로코드 버퍼 (시스템 메모리, DMA 가능 영역): DMAMOV SAR DMAMOV DAR DMAMOV CCR DMALP 256 DMAWFP DMALD DMAST DMALPEND DMASEV → SAR/DAR 설정 → CCR(버스트/폭) → 루프[대기→읽기→쓰기] → 이벤트(인터럽트) DMAGO PC fetch

PL330 주요 마이크로코드 명령어

명령어바이트동작인코딩 예시
DMAMOV6SAR/DAR/CCR 레지스터에 32비트 값 설정BC 00 addr[31:0] (SAR)
DMALD1소스에서 MFIFO로 버스트 읽기04 (single), 05 (burst)
DMAST1MFIFO에서 목적지로 버스트 쓰기08 (single), 09 (burst)
DMALP2루프 카운터 설정 (최대 256)20 FF (256회)
DMALPEND2루프 끝, 카운터 감소 후 분기38 backward_jump
DMAWFP2주변장치 DMA 요청 대기31 periph_id
DMASEV2이벤트/인터럽트 발생34 event_num
DMAGO6채널 시작 (Manager만)A0 ch_num PC[31:0]
DMAKILL1채널 강제 종료01
DMAEND1프로그램 종료00
DMARMB/DMAWMB1읽기/쓰기 메모리 배리어12 / 13

커널의 PL330 코드 생성

/* drivers/dma/pl330.c — 마이크로코드 생성 헬퍼 */
static inline void _emit_MOV(unsigned dry_run, u8 buf[],
    enum dmamov_dst dst, u32 val)
{
    buf[0] = CMD_DMAMOV;
    buf[1] = dst;           /* SAR=0, CCR=1, DAR=2 */
    *(u32 *)&buf[2] = val;
}

/* slave SG 전송을 위한 마이크로코드 시퀀스 생성 */
/* DMAMOV SAR, src_addr
 * DMAMOV DAR, dst_addr
 * DMAMOV CCR, burst_config
 * DMALP lc0, burst_count
 *   DMAWFP periph_id, burst
 *   DMALD
 *   DMAST
 * DMALPEND
 * DMASEV event_num
 * DMAEND */
PL330 디버깅: /sys/kernel/debug/dmaengine/dma0에서 채널 상태, PC(프로그램 카운터), MFIFO 상태를 확인할 수 있습니다. DMAKILL 후에도 MFIFO에 잔여 데이터가 있을 수 있으니 DMAFLUSHP로 정리해야 합니다.

Intel IDXD / DSA

Intel Data Streaming Accelerator (DSA)는 서버급 데이터 이동 가속기로, Intel IDXD(Intel Data Accelerator Driver) 프레임워크를 통해 커널에 통합됩니다. 기존 IOAT를 대체하는 차세대 DMA 엔진입니다.

Intel DSA (Data Streaming Accelerator) 아키텍처 Work Queues Dedicated WQ (커널) Shared WQ (유저/SVA) DSA Engine memcpy / memfill / compare CRC32 / DIF / compress cache flush / batch submit Completion MMIO 폴링 인터럽트 완료 레코드 제출 모델: 커널 모드: DMA Engine API (dmaengine_prep_dma_memcpy) → Dedicated WQ → ENQCMD/MOVDIR64B 유저 모드: ENQCMDS (SVA/PASID) → Shared WQ → 가상 주소 직접 사용 (zero-copy) 배치 모드: 여러 디스크립터를 배치로 묶어 단일 MOVDIR64B로 제출 → 오버헤드 최소화

IOAT vs DSA 비교

항목Intel IOATIntel DSA (IDXD)
세대Crystal Beach 1~3Sapphire Rapids+
제출 방식디스크립터 링 + MMIOENQCMD/MOVDIR64B
연산memcpy, XORmemcpy, memfill, compare, CRC, DIF, compress
유저 모드불가SVA/PASID로 가능
배치미지원배치 디스크립터 지원
캐시DCA (Direct Cache Access)캐시 플러시 연산 지원
NUMA소켓 단위소켓별 독립 DSA 인스턴스
드라이버drivers/dma/ioat/drivers/dma/idxd/

DSA 설정과 사용

# 1. IDXD 드라이버 로드
modprobe idxd

# 2. accel-config 도구로 DSA 설정
# (Intel IDXD 유틸리티 패키지 필요)

# Work Queue 생성 및 설정
accel-config config-wq dsa0/wq0.0 \
  --group-id=0 \
  --mode=dedicated \
  --type=kernel \
  --name="dmaengine" \
  --priority=10 \
  --size=16

# DSA 디바이스 활성화
accel-config config-engine dsa0/engine0.0 --group-id=0
accel-config enable-device dsa0
accel-config enable-wq dsa0/wq0.0

# 3. dmaengine으로 DMA Engine 채널 등록 확인
cat /sys/kernel/debug/dmaengine/summary
# dsa0 (idxd-dma): number of channels: 1
#   dma0chan0 | idle

# 4. dmatest로 성능 확인
modprobe dmatest
echo dma0chan0 > /sys/module/dmatest/parameters/channel
echo 1000 > /sys/module/dmatest/parameters/iterations
echo 1 > /sys/module/dmatest/parameters/run

커널에서 DSA 사용 (DMA Engine API)

/* DSA는 DMA Engine에 자동 등록되므로 기존 API 그대로 사용 */
dma_cap_mask_t mask;
dma_cap_zero(mask);
dma_cap_set(DMA_MEMCPY, mask);

/* IOAT 대신 DSA가 있으면 DSA 채널이 할당됨 */
chan = dma_request_channel(mask, NULL, NULL);
desc = dmaengine_prep_dma_memcpy(chan, dst, src, len,
    DMA_PREP_INTERRUPT);
dmaengine_submit(desc);
dma_async_issue_pending(chan);
DSA 유저 모드: /dev/dsa/wq0.0 캐릭터 디바이스를 통해 유저 공간에서 직접 DSA에 디스크립터를 제출할 수 있습니다. SVA(Shared Virtual Addressing)를 사용하면 가상 주소 그대로 DMA 전송이 가능하여 커널 진입 없이 초저지연 데이터 이동이 가능합니다.

DMA Engine과 DMA 매핑 관계

DMA Engine과 DMA 매핑 API(DMA 심화)는 서로 다른 계층입니다:

구분DMA 매핑 APIDMA Engine
관심사메모리 주소 변환, 캐시 일관성DMA 컨트롤러 추상화, 채널 관리
핵심 APIdma_map_sg(), dma_alloc_coherent()dmaengine_prep_*(), dmaengine_submit()
헤더linux/dma-mapping.hlinux/dmaengine.h
위치kernel/dma/drivers/dma/
IOMMU직접 다룸내부적으로 매핑 API 사용

소프트웨어 스택 계층

클라이언트 드라이버 (SPI, ALSA, md/RAID ...) DMA Engine (dmaengine.c) DMA 컨트롤러 드라이버 (PL330, IOAT, eDMA ...) DMA Mapping API (dma_map_sg ...) HW 레지스터 직접 접근 IOMMU / SWIOTLB / DMA 컨트롤러 하드웨어
핵심: Consumer(클라이언트)는 DMA Engine API만 사용하고, DMA 매핑 API를 직접 호출하지 않습니다. Provider(컨트롤러 드라이버)가 내부적으로 DMA 매핑을 수행합니다. 단, Consumer가 직접 scatter-gather 리스트를 만들 때는 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
dmatest 파라미터:
  • 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 크기 증가, 인터럽트 코얼레싱 확인

성능 최적화 팁

관련 Kconfig 옵션

옵션기본값설명
CONFIG_DMADEVICESyDMA Engine 서브시스템 활성화
CONFIG_DMA_ENGINEyDMA Engine 코어
CONFIG_DMA_VIRTUAL_CHANNELSyvirt-dma 헬퍼
CONFIG_DMA_OFyDevice Tree DMA 매핑
CONFIG_DMATESTnDMA 테스트 모듈
CONFIG_ASYNC_TX_DMAnasync_tx DMA 가속 활성화
CONFIG_INTEL_IOATDMAnIntel IOAT DMA 드라이버
CONFIG_PL330_DMAnARM PL330 DMA 드라이버
CONFIG_TI_EDMAnTI eDMA 드라이버
CONFIG_DW_DMACnSynopsys DW-DMA 드라이버

성능 벤치마킹

DMA Engine 성능을 정량적으로 측정하는 방법과 주요 지표를 소개합니다.

핵심 성능 지표

지표측정 방법일반적 값 (SoC DMA)의미
처리량 (throughput)dmatest iops × 전송 크기100~500 MB/s단위 시간당 전송 데이터량
IOPSdmatest iterations/시간10K~100K ops/s초당 DMA 연산 수 (소규모 전송)
지연 (latency)ftrace dma_issue→dma_complete5~200 μs전송 시작~완료 시간
설정 오버헤드prep+submit 시간1~10 μs디스크립터 준비 + 제출 소요
CPU 사용률top/perf during dmatest<5% (이상적)DMA 중 CPU offload 효과

dmatest 기반 벤치마킹

# 전송 크기별 처리량 측정
for size in 256 1024 4096 16384 65536 262144; do
    echo $size > /sys/module/dmatest/parameters/test_buf_size
    echo 1000 > /sys/module/dmatest/parameters/iterations
    echo 1 > /sys/module/dmatest/parameters/threads_per_chan
    echo dma0chan0 > /sys/module/dmatest/parameters/channel
    echo 1 > /sys/module/dmatest/parameters/run

    # 결과 대기
    sleep 5
    dmesg | tail -1
    # dma0chan0-copy0: summary 1000 tests, 0 failures NNNNN iops NNNNN KB/s
done

# 멀티 채널 동시 테스트 (총 대역폭 측정)
echo "" > /sys/module/dmatest/parameters/channel
echo 8 > /sys/module/dmatest/parameters/max_channels
echo 65536 > /sys/module/dmatest/parameters/test_buf_size
echo 1 > /sys/module/dmatest/parameters/run

perf를 이용한 DMA 분석

# DMA 함수 호출 프로파일링
perf record -g -a -- sleep 10
# (10초간 DMA 작업 수행)
perf report --sort comm,dso,symbol | grep dma

# DMA 인터럽트 빈도 측정
watch -n 1 "cat /proc/interrupts | grep dma"

# CPU 사이클 대비 DMA offload 효과
perf stat -e cycles,instructions,cache-misses -- \
    dd if=/dev/zero of=/dev/null bs=64K count=10000

CPU memcpy vs DMA 크로스오버 분석

소규모 전송에서는 CPU memcpy가 DMA보다 빠릅니다. DMA 설정 오버헤드(디스크립터 생성, 매핑, 인터럽트) 때문입니다.

전송 크기CPU memcpyDMA Engine승자
< 256B~50 ns~5 μsCPU (100×)
1 KB~200 ns~5 μsCPU (25×)
4 KB~800 ns~6 μsCPU (7.5×)
64 KB~12 μs~10 μsDMA (CPU free)
1 MB~200 μs~50 μsDMA (4×, CPU free)
16 MB+~3 ms~800 μsDMA (3.8×, CPU free)
크로스오버 포인트: 일반적으로 4KB~64KB 사이에서 DMA가 CPU memcpy보다 효율적이 됩니다. 정확한 크로스오버 포인트는 DMA 컨트롤러, 메모리 대역폭, 캐시 계층에 따라 다릅니다. net.core.dma_copybreak처럼 커널에서도 전송 크기에 따라 CPU/DMA를 자동 선택하는 패턴이 있습니다.

완전한 Provider 드라이버 구현

virt-dma 헬퍼를 사용하는 최소이면서 완전한 DMA 컨트롤러 드라이버 골격입니다. 실제 하드웨어 레지스터 접근을 가상으로 대체한 교육용 예제입니다.

자료구조 정의

#include <linux/dmaengine.h>
#include <linux/of_dma.h>
#include <linux/platform_device.h>
#include <linux/pm_runtime.h>
#include "virt-dma.h"

#define MY_DMA_MAX_CHANS    8
#define MY_DMA_MAX_BURST    256

struct my_dma_desc {
    struct virt_dma_desc vd;    /* 반드시 첫 필드 */
    dma_addr_t src;
    dma_addr_t dst;
    size_t len;
    size_t residue;
    enum dma_transfer_direction dir;
    struct dma_slave_config cfg;
    struct scatterlist *sg;
    unsigned int sg_len;
    unsigned int sg_idx;
};

struct my_dma_chan {
    struct virt_dma_chan vchan;  /* 반드시 첫 필드 */
    void __iomem *base;
    int id;
    struct my_dma_desc *cur_desc;
    struct dma_slave_config cfg;
    bool configured;
};

struct my_dma_dev {
    struct dma_device dma_dev;
    void __iomem *base;
    struct clk *clk;
    int irq;
    struct my_dma_chan chans[MY_DMA_MAX_CHANS];
    int nr_channels;
};

헬퍼 매크로와 함수

static inline struct my_dma_chan *to_my_chan(
    struct dma_chan *chan)
{
    return container_of(chan, struct my_dma_chan,
        vchan.chan);
}

static inline struct my_dma_desc *to_my_desc(
    struct virt_dma_desc *vd)
{
    return container_of(vd, struct my_dma_desc, vd);
}

static inline struct my_dma_dev *to_my_dma(
    struct dma_device *dma)
{
    return container_of(dma, struct my_dma_dev, dma_dev);
}

콜백 구현

/* 디스크립터 해제 콜백 (virt-dma 필수) */
static void my_desc_free(struct virt_dma_desc *vd)
{
    kfree(to_my_desc(vd));
}

/* 채널 리소스 할당 */
static int my_alloc_chan_resources(
    struct dma_chan *chan)
{
    struct my_dma_chan *mchan = to_my_chan(chan);
    struct my_dma_dev *mdev = to_my_dma(chan->device);

    pm_runtime_get_sync(mdev->dma_dev.dev);
    mchan->configured = false;
    return 0;
}

/* 채널 리소스 해제 */
static void my_free_chan_resources(
    struct dma_chan *chan)
{
    struct my_dma_dev *mdev = to_my_dma(chan->device);

    vchan_free_chan_resources(to_virt_chan(chan));
    pm_runtime_mark_last_busy(mdev->dma_dev.dev);
    pm_runtime_put_autosuspend(mdev->dma_dev.dev);
}

/* 슬레이브 설정 */
static int my_device_config(struct dma_chan *chan,
    struct dma_slave_config *config)
{
    struct my_dma_chan *mchan = to_my_chan(chan);

    memcpy(&mchan->cfg, config, sizeof(*config));
    mchan->configured = true;
    return 0;
}

/* Slave SG 디스크립터 생성 */
static struct dma_async_tx_descriptor *
my_prep_slave_sg(struct dma_chan *chan,
    struct scatterlist *sgl, unsigned int sg_len,
    enum dma_transfer_direction dir,
    unsigned long flags, void *context)
{
    struct my_dma_chan *mchan = to_my_chan(chan);
    struct my_dma_desc *desc;
    struct scatterlist *sg;
    size_t total = 0;
    int i;

    if (!mchan->configured)
        return NULL;

    desc = kzalloc(sizeof(*desc), GFP_NOWAIT);
    if (!desc)
        return NULL;

    desc->dir = dir;
    desc->sg = sgl;
    desc->sg_len = sg_len;
    desc->sg_idx = 0;
    memcpy(&desc->cfg, &mchan->cfg, sizeof(mchan->cfg));

    for_each_sg(sgl, sg, sg_len, i)
        total += sg_dma_len(sg);
    desc->len = total;
    desc->residue = total;

    return vchan_tx_prep(&mchan->vchan, &desc->vd, flags);
}

/* 하드웨어에 전송 시작 */
static void my_start_transfer(struct my_dma_chan *mchan)
{
    struct virt_dma_desc *vd;
    struct my_dma_desc *desc;

    vd = vchan_next_desc(&mchan->vchan);
    if (!vd) {
        mchan->cur_desc = NULL;
        return;
    }

    list_del(&vd->node);
    desc = to_my_desc(vd);
    mchan->cur_desc = desc;

    /* 하드웨어 레지스터 프로그래밍 */
    /* writel(desc->src, mchan->base + SRC_ADDR);
     * writel(desc->dst, mchan->base + DST_ADDR);
     * writel(desc->len, mchan->base + XFER_SIZE);
     * writel(START_BIT, mchan->base + CTRL_REG); */
}

/* issue_pending: 보류 중인 전송을 HW에 전달 */
static void my_issue_pending(struct dma_chan *chan)
{
    struct my_dma_chan *mchan = to_my_chan(chan);
    unsigned long flags;

    spin_lock_irqsave(&mchan->vchan.lock, flags);
    if (vchan_issue_pending(&mchan->vchan) &&
        !mchan->cur_desc)
        my_start_transfer(mchan);
    spin_unlock_irqrestore(&mchan->vchan.lock, flags);
}

/* 전송 상태 조회 */
static enum dma_status my_tx_status(
    struct dma_chan *chan,
    dma_cookie_t cookie,
    struct dma_tx_state *txstate)
{
    struct my_dma_chan *mchan = to_my_chan(chan);
    enum dma_status ret;
    unsigned long flags;

    ret = dma_cookie_status(chan, cookie, txstate);
    if (ret == DMA_COMPLETE || !txstate)
        return ret;

    spin_lock_irqsave(&mchan->vchan.lock, flags);
    if (mchan->cur_desc &&
        mchan->cur_desc->vd.tx.cookie == cookie)
        dma_set_residue(txstate,
            mchan->cur_desc->residue);
    spin_unlock_irqrestore(&mchan->vchan.lock, flags);

    return ret;
}

/* 모든 전송 종료 */
static int my_terminate_all(struct dma_chan *chan)
{
    struct my_dma_chan *mchan = to_my_chan(chan);
    unsigned long flags;
    LIST_HEAD(head);

    spin_lock_irqsave(&mchan->vchan.lock, flags);
    /* HW 중지: writel(STOP_BIT, mchan->base + CTRL_REG); */
    mchan->cur_desc = NULL;
    vchan_get_all_descriptors(&mchan->vchan, &head);
    spin_unlock_irqrestore(&mchan->vchan.lock, flags);

    vchan_dma_desc_free_list(&mchan->vchan, &head);
    return 0;
}

인터럽트 핸들러

static irqreturn_t my_dma_irq(int irq, void *data)
{
    struct my_dma_dev *mdev = data;
    u32 status;
    int i;

    /* 인터럽트 상태 읽기 */
    /* status = readl(mdev->base + INT_STATUS); */
    status = 0xFF; /* 예시 */

    for (i = 0; i < mdev->nr_channels; i++) {
        if (!(status & BIT(i)))
            continue;

        struct my_dma_chan *mchan = &mdev->chans[i];

        spin_lock(&mchan->vchan.lock);
        if (mchan->cur_desc) {
            /* 전송 완료: virt-dma에 알림 */
            vchan_cookie_complete(&mchan->cur_desc->vd);
            mchan->cur_desc = NULL;
            /* 다음 대기 전송 시작 */
            my_start_transfer(mchan);
        }
        spin_unlock(&mchan->vchan.lock);

        /* 인터럽트 ACK */
        /* writel(BIT(i), mdev->base + INT_CLEAR); */
    }

    return IRQ_HANDLED;
}

probe / remove

static int my_dma_probe(struct platform_device *pdev)
{
    struct my_dma_dev *mdev;
    struct dma_device *dd;
    int ret, i;

    mdev = devm_kzalloc(&pdev->dev,
        sizeof(*mdev), GFP_KERNEL);
    if (!mdev)
        return -ENOMEM;

    mdev->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(mdev->base))
        return PTR_ERR(mdev->base);

    mdev->irq = platform_get_irq(pdev, 0);
    if (mdev->irq < 0)
        return mdev->irq;

    mdev->clk = devm_clk_get(&pdev->dev, NULL);
    if (IS_ERR(mdev->clk))
        return PTR_ERR(mdev->clk);

    ret = clk_prepare_enable(mdev->clk);
    if (ret)
        return ret;

    /* DMA 디바이스 설정 */
    dd = &mdev->dma_dev;
    dd->dev = &pdev->dev;
    dd->src_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_1_BYTE) |
        BIT(DMA_SLAVE_BUSWIDTH_2_BYTES) |
        BIT(DMA_SLAVE_BUSWIDTH_4_BYTES);
    dd->dst_addr_widths = dd->src_addr_widths;
    dd->directions = BIT(DMA_DEV_TO_MEM) |
        BIT(DMA_MEM_TO_DEV);
    dd->residue_granularity =
        DMA_RESIDUE_GRANULARITY_BURST;
    dd->max_burst = MY_DMA_MAX_BURST;

    dma_cap_set(DMA_SLAVE, dd->cap_mask);
    dma_cap_set(DMA_CYCLIC, dd->cap_mask);
    dma_cap_set(DMA_PRIVATE, dd->cap_mask);

    /* 콜백 등록 */
    dd->device_alloc_chan_resources = my_alloc_chan_resources;
    dd->device_free_chan_resources = my_free_chan_resources;
    dd->device_config = my_device_config;
    dd->device_prep_slave_sg = my_prep_slave_sg;
    dd->device_issue_pending = my_issue_pending;
    dd->device_tx_status = my_tx_status;
    dd->device_terminate_all = my_terminate_all;

    /* 채널 초기화 */
    mdev->nr_channels = MY_DMA_MAX_CHANS;
    INIT_LIST_HEAD(&dd->channels);
    for (i = 0; i < mdev->nr_channels; i++) {
        struct my_dma_chan *mchan = &mdev->chans[i];
        mchan->id = i;
        mchan->base = mdev->base + i * 0x100;
        mchan->vchan.desc_free = my_desc_free;
        vchan_init(&mchan->vchan, dd);
    }

    /* 인터럽트 등록 */
    ret = devm_request_irq(&pdev->dev, mdev->irq,
        my_dma_irq, IRQF_SHARED,
        dev_name(&pdev->dev), mdev);
    if (ret)
        goto err_clk;

    /* DMA Engine 등록 */
    ret = dma_async_device_register(dd);
    if (ret)
        goto err_clk;

    /* Device Tree xlate 등록 */
    ret = of_dma_controller_register(pdev->dev.of_node,
        of_dma_xlate_by_chan_id, mdev);
    if (ret)
        goto err_unreg;

    platform_set_drvdata(pdev, mdev);

    /* Runtime PM */
    pm_runtime_set_autosuspend_delay(&pdev->dev, 500);
    pm_runtime_use_autosuspend(&pdev->dev);
    pm_runtime_set_active(&pdev->dev);
    pm_runtime_enable(&pdev->dev);

    dev_info(&pdev->dev,
        "My DMA controller with %d channels\n",
        mdev->nr_channels);
    return 0;

err_unreg:
    dma_async_device_unregister(dd);
err_clk:
    clk_disable_unprepare(mdev->clk);
    return ret;
}

static int my_dma_remove(struct platform_device *pdev)
{
    struct my_dma_dev *mdev =
        platform_get_drvdata(pdev);

    pm_runtime_disable(&pdev->dev);
    of_dma_controller_free(pdev->dev.of_node);
    dma_async_device_unregister(&mdev->dma_dev);
    clk_disable_unprepare(mdev->clk);
    return 0;
}

static const struct of_device_id my_dma_of_match[] = {
    { .compatible = "vendor,my-dma-1.0" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_dma_of_match);

static struct platform_driver my_dma_driver = {
    .probe  = my_dma_probe,
    .remove = my_dma_remove,
    .driver = {
        .name = "my-dma",
        .of_match_table = my_dma_of_match,
        .pm = &my_dma_pm_ops,
    },
};
module_platform_driver(my_dma_driver);
Provider 체크리스트:
  • virt-dma.h 사용 — 디스크립터 관리, 쿠키 추적 자동화
  • DMA_PRIVATE 설정 — SoC DMA는 범용 memcpy에 사용하지 않음
  • residue_granularity 설정 — Consumer가 잔여량 정밀도 파악
  • of_dma_controller_register() — Device Tree 바인딩 등록
  • ✓ Runtime PM — 유휴 채널 클럭 차단
  • vchan_cookie_complete() — 인터럽트에서 완료 알림
  • vchan_get_all_descriptors() — terminate_all에서 안전한 정리

DMA Engine API 빠른 참조

Consumer API 요약

함수용도컨텍스트
dma_request_chan()DT/ACPI 기반 채널 요청process
dma_release_channel()채널 해제process
dmaengine_slave_config()슬레이브 설정process
dmaengine_prep_slave_sg()SG 디스크립터 생성any
dmaengine_prep_dma_cyclic()Cyclic 디스크립터 생성any
dmaengine_prep_dma_memcpy()Memcpy 디스크립터 생성any
dmaengine_prep_interleaved_dma()Interleaved 디스크립터 생성any
dmaengine_submit()디스크립터 큐에 제출any
dma_async_issue_pending()보류 전송 HW 전달any
dmaengine_tx_status()전송 상태/residue 조회any
dmaengine_terminate_async()비동기 전송 종료any (콜백 OK)
dmaengine_terminate_sync()동기 전송 종료process only
dmaengine_synchronize()콜백 완료 동기화process only
dmaengine_pause()/resume()전송 일시정지/재개any

Provider API 요약

함수용도
dma_async_device_register()DMA 디바이스 등록
dma_async_device_unregister()DMA 디바이스 해제
of_dma_controller_register()DT xlate 콜백 등록
of_dma_controller_free()DT xlate 해제
dma_cookie_init()쿠키 초기화
dma_cookie_assign()디스크립터에 쿠키 할당
dma_cookie_complete()쿠키 완료 표시
dma_cookie_status()쿠키 상태 조회
dma_set_residue()잔여량 설정
vchan_init()virt-dma 채널 초기화
vchan_tx_prep()virt-dma 디스크립터 준비
vchan_issue_pending()보류 디스크립터 issued로 이동
vchan_cookie_complete()완료 처리 + tasklet 스케줄
vchan_next_desc()다음 issued 디스크립터 가져오기
vchan_get_all_descriptors()모든 디스크립터 리스트 추출
vchan_dma_desc_free_list()디스크립터 리스트 해제

DMA Engine과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.