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 */
DMA 컨트롤러 하드웨어 내부 구조
SoC DMA 컨트롤러는 일반적으로 다음 구성 요소를 포함합니다. 컨트롤러마다 세부 구현은 다르지만 기본 구조는 동일합니다.
| 구성 요소 | 역할 | 예시 |
|---|---|---|
| 채널 (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 상태에 맞추기 위해 하드웨어 핸드셰이크 신호를 사용합니다.
| 설정 | dma_slave_config 필드 | 의미 |
|---|---|---|
| 버스트 크기 | src_maxburst / dst_maxburst | 한 번의 DREQ에 전송할 데이터 단위 수 (FIFO depth의 절반이 일반적) |
| 버스 폭 | src_addr_width / dst_addr_width | 1/2/4/8/16/32/64 바이트 (FIFO 레지스터 폭에 맞춤) |
| 흐름 제어 | device_fc | true: 페리퍼럴이 전송 길이 제어, false: DMA 컨트롤러가 제어 |
| FIFO 주소 | src_addr / dst_addr | 페리퍼럴 FIFO의 물리 주소 (가상 주소 아님!) |
src_addr/dst_addr에 ioremap()으로 얻은 가상 주소를 전달하면 안 됩니다. DMA 컨트롤러는 CPU MMU를 거치지 않으므로 반드시 물리 주소를 사용해야 합니다. 플랫폼 디바이스에서는 platform_get_resource()로 물리 주소를 얻으십시오.흐름 제어 (Flow Controller)
DMA 전송의 길이를 누가 결정하느냐에 따라 흐름 제어 모드가 나뉩니다:
| 모드 | device_fc | 전송 길이 결정 | 사용 사례 |
|---|---|---|---|
| DMA-controlled | false (기본) | DMA 컨트롤러가 카운터 관리 | SPI, I2C, UART (길이 사전 알려짐) |
| Peripheral-controlled | true | 페리퍼럴이 TC 신호로 종료 통보 | SD/MMC (블록 크기가 가변), NAND |
아키텍처
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; /* 디바이스 흐름 제어 */
...
};
virt-dma 헬퍼 프레임워크
drivers/dma/virt-dma.h의 virt-dma 헬퍼는 대부분의 DMA 컨트롤러 드라이버가 사용하는 디스크립터 관리 프레임워크입니다. 가상 채널(struct virt_dma_chan)과 가상 디스크립터(struct virt_dma_desc)를 통해 제출/발행/완료 큐 관리, tasklet 기반 콜백 처리를 자동화합니다.
/* 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 스케줄 */
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_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);
DMAMUX (DMA Request Multiplexer)
최신 SoC에서는 주변장치 수가 DMA 채널 수보다 훨씬 많습니다. DMAMUX는 N개의 DMA 요청 소스를 M개의 DMA 채널에 동적으로 라우팅하는 하드웨어 멀티플렉서입니다.
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/MP1 | dma/stm32-dmamux.c | 107 | 16 (DMA1+DMA2) | 동기 생성기, 요청 동기화 |
| NXP i.MX8 | dma/fsl-edma.c 내장 | 64+ | 32 | DMAMUX 레지스터 직접 제어 |
| Renesas RZ/G2 | dma/sh/rcar-dmac.c | SoC별 | 15 | MID-RID 기반 매핑 |
| TI AM62x | dma/ti/k3-psil.c | PSIL thread | BCDMA/PKTDMA | PSI-L fabric 라우팅 |
채널 필터링과 xlate
DMA Engine 프레임워크는 Device Tree/ACPI에서 DMA 채널을 자동으로 매핑합니다. 이 과정의 핵심은 of_dma_xlate 콜백입니다.
xlate 처리 흐름
xlate 유형
| xlate 함수 | #dma-cells | 용도 | 사용 컨트롤러 |
|---|---|---|---|
of_dma_xlate_by_chan_id | 1 | 셀 값 = 채널 번호 | PL330, DW-DMA |
커스텀 xlate | 2~4 | 요청 번호 + 설정 인코딩 | STM32 DMA, TI eDMA, K3 UDMA |
of_dma_simple_xlate | 1 | 단순 채널 매핑 | 범용 |
커스텀 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 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과 함께 사용) |
Interleaved DMA (2D/3D 전송)
Interleaved DMA는 비연속 메모리 패턴(스트라이드, 2D 블록 전송)을 단일 DMA 연산으로 처리합니다. 프레임버퍼 전송, 이미지 로테이션, 행렬 전치 등에서 CPU 개입 없이 복잡한 메모리 접근 패턴을 실현합니다.
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_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로 전송합니다. 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_SEGMENT | SG 세그먼트 단위 | 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)
};
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() | 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>;
};
PL330 마이크로코드 아키텍처
ARM PL330 (DMA-330)은 독특하게 마이크로 프로그래밍 방식으로 동작합니다. DMA 전송이 하드웨어 디스크립터 체인 대신 마이크로 코드 명령어로 제어됩니다.
PL330 주요 마이크로코드 명령어
| 명령어 | 바이트 | 동작 | 인코딩 예시 |
|---|---|---|---|
DMAMOV | 6 | SAR/DAR/CCR 레지스터에 32비트 값 설정 | BC 00 addr[31:0] (SAR) |
DMALD | 1 | 소스에서 MFIFO로 버스트 읽기 | 04 (single), 05 (burst) |
DMAST | 1 | MFIFO에서 목적지로 버스트 쓰기 | 08 (single), 09 (burst) |
DMALP | 2 | 루프 카운터 설정 (최대 256) | 20 FF (256회) |
DMALPEND | 2 | 루프 끝, 카운터 감소 후 분기 | 38 backward_jump |
DMAWFP | 2 | 주변장치 DMA 요청 대기 | 31 periph_id |
DMASEV | 2 | 이벤트/인터럽트 발생 | 34 event_num |
DMAGO | 6 | 채널 시작 (Manager만) | A0 ch_num PC[31:0] |
DMAKILL | 1 | 채널 강제 종료 | 01 |
DMAEND | 1 | 프로그램 종료 | 00 |
DMARMB/DMAWMB | 1 | 읽기/쓰기 메모리 배리어 | 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 */
/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 엔진입니다.
IOAT vs DSA 비교
| 항목 | Intel IOAT | Intel DSA (IDXD) |
|---|---|---|
| 세대 | Crystal Beach 1~3 | Sapphire Rapids+ |
| 제출 방식 | 디스크립터 링 + MMIO | ENQCMD/MOVDIR64B |
| 연산 | memcpy, XOR | memcpy, 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);
/dev/dsa/wq0.0 캐릭터 디바이스를 통해 유저 공간에서 직접 DSA에 디스크립터를 제출할 수 있습니다. SVA(Shared Virtual Addressing)를 사용하면 가상 주소 그대로 DMA 전송이 가능하여 커널 진입 없이 초저지연 데이터 이동이 가능합니다.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 성능을 정량적으로 측정하는 방법과 주요 지표를 소개합니다.
핵심 성능 지표
| 지표 | 측정 방법 | 일반적 값 (SoC DMA) | 의미 |
|---|---|---|---|
| 처리량 (throughput) | dmatest iops × 전송 크기 | 100~500 MB/s | 단위 시간당 전송 데이터량 |
| IOPS | dmatest iterations/시간 | 10K~100K ops/s | 초당 DMA 연산 수 (소규모 전송) |
| 지연 (latency) | ftrace dma_issue→dma_complete | 5~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 memcpy | DMA Engine | 승자 |
|---|---|---|---|
| < 256B | ~50 ns | ~5 μs | CPU (100×) |
| 1 KB | ~200 ns | ~5 μs | CPU (25×) |
| 4 KB | ~800 ns | ~6 μs | CPU (7.5×) |
| 64 KB | ~12 μs | ~10 μs | DMA (CPU free) |
| 1 MB | ~200 μs | ~50 μs | DMA (4×, CPU free) |
| 16 MB+ | ~3 ms | ~800 μs | DMA (3.8×, CPU free) |
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);
- ✓
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과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.