이번엔 지난 글에서 다룬 PCI 장치를 제어하기 위한 드라이버와 유저 앱을 작성해 보겠다. 편의상 MMIO + INTx 방식의 장치를 v1, DMA + MSI 방식의 장치를 v2로 구분한다.
드라이버와 유저 앱을 설계할 때 고려한 것은 크게 두 가지다.
1. zero-copy* 구현
2. 유저 앱까지 이어지는 파이프라이닝 구조
✓ Zero-copy란? 데이터를 주고받을 때, CPU가 개입해서 복사하는 횟수를 0회로 만든다는 뜻이다.
먼저 1번.
지난번에 작성한 v1은 zero-copy가 아니었다. 드라이버 단에서 mmap을 사용해 커널과 유저 스페이스 사이의 메모리 복사(copy_to_user, copy_from_user)는 없앴지만, mmap한 대상이 시스템 RAM이 아닌 장치의 BAR 공간으로, 애플리케이션이 일반 메모리처럼 자유롭게 연산에 활용할 수 없기 때문에, 결국 데이터를 활용하려면 유저 공간(RAM)으로 CPU가 직접 memcpy를 수행해 복사해 와야만 했다.
반면, 이번 v2에서는 드라이버가 시스템 RAM에 DMA 가능한 영역을 할당(dma_alloc_coherent)하고 이를 mmap으로 유저 공간에 매핑한다. 이렇게 하면 장치는 DMA로 이 RAM에 직접 데이터를 꽂아 넣고, 유저 애플리케이션도 중간 복사(memcpy) 없이 이 RAM에 있는 데이터를 일반 메모리처럼 직접 읽고 쓰며 연산할 수 있다. 즉, CPU의 개입을 없애 완전한 Zero-copy를 구현한 것이다.
그 다음 2번.
지난 번에는 poll 콜백을 구현해두긴 했지만 유저 앱에선 사용하지 않았고, WAIT_DONE이라는 ioctl을 호출해 블락킹 방식으로 대기하게 했다. 하지만 한 프레임 작업이 완료될 때까지 대기하고 다음 작업을 제출하는 방식이라면 굳이 장치와 드라이버에 DMA + SQ/CQ 구조를 적용한 보람이 없기 때문에, 이번엔 이미지가 아니라 영상을 사용해 연속적인 프레임 처리를 시험해보려고 한다.
v1 드라이버와의 차이를 기준으로 설명한다.
1. uapi 헤더 수정: REQBUFS / QBUF / DQBUF
v2는 V4L2 스타일의 버퍼 풀 방식을 사용하며, 이를 위해 ioctl에서 세 가지 커맨드를 지원한다.
#pragma once
#include <linux/ioctl.h>
#include <linux/types.h>
#define VIP_IOC_MAGIC 'V'
enum vip_format {
VIP_FMT_RGB24 = 0,
VIP_FMT_BGR24 = 1,
};
enum vip_filter {
VIP_FILT_GRAY = 0,
VIP_FILT_INVERT = 1,
VIP_FILT_BRIGHT = 2,
};
enum vip_state {
VIP_STATE_IDLE = 0,
VIP_STATE_BUSY = 1,
VIP_STATE_DONE = 2,
VIP_STATE_ERR = 3,
};
struct vip_config {
__u32 width;
__u32 height;
__u32 format;
__u32 filter;
__s32 param;
};
struct vip_status {
__u32 status;
};
struct vip_wait {
__u32 timeout_ms;
__u32 status;
};
#define VIP_IOCTL_GET_ID _IOR(VIP_IOC_MAGIC, 0x06, __u32)
#define VIP_IOCTL_CONFIG _IOW(VIP_IOC_MAGIC, 0x01, struct vip_config)
#define VIP_IOCTL_START _IO(VIP_IOC_MAGIC, 0x02)
#define VIP_IOCTL_GET_STATUS _IOR(VIP_IOC_MAGIC, 0x03, struct vip_status)
#define VIP_IOCTL_WAIT_DONE _IOWR(VIP_IOC_MAGIC, 0x04, struct vip_wait)
#define VIP_IOCTL_ACK_IRQ _IO(VIP_IOC_MAGIC, 0x05)
1.1. REQBUFS
앱이 먼저 REQBUFS ioctl을 호출해서 필요한 버퍼의 수(count)와 개별 크기(buf_size)를 요청하면, 드라이버가 dma_alloc_coherent() 로 DMA 접근이 가능한 버퍼를 미리 할당한다.
앱은 할당된 버퍼의 절반은 입력용 나머지 절반은 출력용으로 사용하는데, 이후 호출할 mmap()에서 offset에 index * buf_size를 넘기면, 드라이버는 이 값을 보고 버퍼 인덱스를 역산해 유저 공간에 맵핑해준다.
case VIP_DMA_IOCTL_REQBUFS: {
// ...
req.buf_size = PAGE_ALIGN(req.buf_size);
buf_infos = kcalloc(req.count, sizeof(*buf_infos), GFP_KERNEL);
if (!buf_infos) {
mutex_unlock(&v->lock);
return -ENOMEM;
}
for (i = 0; i < req.count; i++) {
buf_infos[i].cpu_addr = dma_alloc_coherent(&v->pdev->dev, req.buf_size, &buf_infos[i].dma_addr, GFP_KERNEL);
// ...
}
v->buf_infos = buf_infos;
v->buf_count = req.count;
v->buf_size = req.buf_size;
// ...
break;
}
static int vip_dma_mmap(struct file *filp, struct vm_area_struct *vma)
{
struct vip_dev *v = filp->private_data;
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
unsigned long size = vma->vm_end - vma->vm_start;
__u32 idx;
if (!v->buf_infos)
return -ENXIO;
idx = offset / v->buf_size;
if (idx >= v->buf_count || size > v->buf_size)
return -EINVAL;
// dma_mmap_coherent()는 DMA coherent 메모리를 유저 공간에 매핑하는 함수로
// 내부에선 적절한 pgprot 플래그 설정과 remap_pfn_range() 호출을 통해 매핑을 수행함
// vm_pgoff를 0으로 초기화해야 버퍼 시작부터 정상 매핑됨
vma->vm_pgoff = 0;
return dma_mmap_coherent(&v->pdev->dev, vma,
v->buf_infos[idx].cpu_addr,
v->buf_infos[idx].dma_addr,
size);
}
1.2. QBUF
QBUF는 작업 제출 ioctl이다. 드라이버는 이 커맨드를 받으면 SQ 슬롯에 디스크립터를 작성하고 REG_SQ_TAIL을 write해서 장치에 도어벨을 울린다.
case VIP_DMA_IOCTL_QBUF: {
// ...
// SQ 꽉 찼는지 확인 (sq_next == sq_head 이면 full)
sq_head = vip_reg_read(v, REG_SQ_HEAD);
sq_next = (v->sq_tail + 1) % VIP_QUEUE_DEPTH;
if (sq_next == sq_head) {
mutex_unlock(&v->lock);
return -EBUSY;
}
// SQ 디스크립터 작성
desc = &v->sq[v->sq_tail];
desc->in_addr = v->buf_infos[qbuf.in_index].dma_addr; // 장치가 DMA로 읽을 주소
desc->out_addr = v->buf_infos[qbuf.out_index].dma_addr; // 장치가 DMA로 쓸 주소
desc->width = qbuf.width;
desc->height = qbuf.height;
desc->format = qbuf.format;
desc->filter = qbuf.filter;
desc->param = qbuf.param < 0 ?
(0x100u | (__u32)(-qbuf.param & 0xFF))
: (__u32)(qbuf.param & 0xFF);
desc->id = qbuf.job_id;
// CQ의 sq_head를 보고 유저 버퍼의 out_index를 알아낼 수 있도록 SQ 슬롯 인덱스와 out_index 매핑 정보 기록
v->jobs[v->sq_tail].out_index = qbuf.out_index;
v->jobs[v->sq_tail].valid = true;
// sq_tail 전진 후 장치에 도어벨 (REG_SQ_TAIL 쓰기)
v->sq_tail = sq_next;
vip_reg_write(v, REG_SQ_TAIL, v->sq_tail);
break;
}
1.3. DQBUF
DQBUF는 완료 수집 ioctl이다. 드라이버는 장치로부터 MSI 인터럽트를 받으면 IRQ 핸들러에서 done_queue에 완료된 작업 정보를 쌓아두고 wake_up_interruptible()로 poll 대기 중인 앱을 깨운다.
유저 앱은 poll에서 깨어나면 DQBUF ioctl을 반복 호출해 done_queue를 소비하고, -EAGAIN이 리턴되면 더 이상 완료된 작업이 없다는 뜻이므로 요청한 작업이 없을 때까지 다시 poll 대기를 반복한다.
case VIP_DMA_IOCTL_DQBUF: {
struct vip_dqbuf result;
unsigned long flags;
// done_queue에서 완료 결과 하나를 꺼냄 (비어있으면 -EAGAIN 리턴)
spin_lock_irqsave(&v->cq_lock, flags);
if (v->dq_count == 0) {
spin_unlock_irqrestore(&v->cq_lock, flags);
mutex_unlock(&v->lock);
return -EAGAIN;
}
result = v->done_queue[v->dq_head];
v->dq_head = (v->dq_head + 1) % VIP_QUEUE_DEPTH;
v->dq_count--;
spin_unlock_irqrestore(&v->cq_lock, flags);
if (copy_to_user((void __user *)arg, &result, sizeof(result))) {
mutex_unlock(&v->lock);
return -EFAULT;
}
break;
}
2. probe: DMA 사용을 위한 초기화 작업
v1 드라이버의 probe와 비교하면 DMA 사용을 위한 초기화 작업이 몇 가지 추가된다.
// 3. DMA 버스 마스터 활성화 (장치가 DMA 트랜잭션을 개시할 수 있도록)
pci_set_master(pdev);
먼저 pci_set_master()를 호출해 PCIe Config Space의 Command Register(0x04)에서 Bus Master Enable 비트(Bit 2)를 1로 set한다. 이 비트가 켜져야 장치의 DMA 엔진이 PCIe 버스에서 트랜잭션을 시작할 수 있다.

// 4. DMA 마스크 설정 (ARMv7 32비트 환경)
ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));
dma_set_mask_and_coherent()는 이 장치가 접근할 수 있는 물리 주소 범위를 커널에 알려주는 함수다. 32비트 환경이므로 DMA_BIT_MASK(32)로 설정하면, 이후 dma_alloc_coherent() 호출 시 장치가 접근 가능한 영역 안에서 메모리를 할당해준다.
그다음엔 SQ와 CQ 배열을 시스템 RAM에 할당하고, 물리 주소를 장치 레지스터에 써서 알려준다. 프레임 버퍼 뿐만 아니라 SQ/CQ 역시 장치가 직접 접근해야 하므로 DMA 가능 영역에 할당해야 한다.
// 9. SQ DMA 버퍼 할당 (0으로 초기화됨)
v->sq = dma_alloc_coherent(&pdev->dev,
VIP_QUEUE_DEPTH * sizeof(struct vip_sq_desc),
&v->sq_dma, GFP_KERNEL);
if (!v->sq) {
ret = -ENOMEM;
goto err_iounmap;
}
// 10. CQ DMA 버퍼 할당 (0으로 초기화됨)
v->cq = dma_alloc_coherent(&pdev->dev,
VIP_QUEUE_DEPTH * sizeof(struct vip_cq_entry),
&v->cq_dma, GFP_KERNEL);
if (!v->cq) {
ret = -ENOMEM;
goto err_sq;
}
// 11. 장치에 SQ/CQ 물리 주소 및 크기 등록
vip_reg_write(v, REG_SQ_BASE_L, lower_32_bits(v->sq_dma));
vip_reg_write(v, REG_SQ_BASE_H, upper_32_bits(v->sq_dma));
vip_reg_write(v, REG_SQ_SIZE, VIP_QUEUE_DEPTH);
vip_reg_write(v, REG_CQ_BASE_L, lower_32_bits(v->cq_dma));
vip_reg_write(v, REG_CQ_BASE_H, upper_32_bits(v->cq_dma));
vip_reg_write(v, REG_CQ_SIZE, VIP_QUEUE_DEPTH);
dma_alloc_coherent()에서 coherent가 '일관성 있는' 이란 뜻인데, 이 함수를 호출하면 CPU와 장치 모두 캐시 일관성 걱정 없이 접근할 수 있는 메모리 영역을 할당하면서, 리턴값으로는 CPU 가상 주소(cpu_addr)를 리턴하고, out 파라미터로는 장치가 DMA로 접근할 물리 주소(dma_addr)를 동시에 반환한다.
참고로 여기서의 cpu_addr은 Kernel space의 가상 주소이기 때문에 mmap으로 맵핑하는 User space의 가상 주소와는 별개지만 mmap 시 결국 둘다 같은 물리 page를 가르키게 된다.
3. probe: MSI 인터럽트 벡터 할당 및 핸들러 등록
// 12. MSI 인터럽트 벡터 최소/최대 1개 할당
ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);
if (ret < 0)
goto err_cq;
// 13. IRQ 핸들러 등록 (MSI는 다른 장치와 IRQ 라인을 공유하지 않고, 해당 장치 전용이므로 IRQF_SHARED 플래그 설정 불필요)
ret = request_irq(pci_irq_vector(pdev, 0), vip_dma_irq_handler, 0, DRV_NAME, v);
if (ret)
goto err_irq_vectors;
pci_alloc_irq_vectors()를 호출하면 PCIe Config space의 MSI Capability 구조체를 설정해서 장치가 MSI를 쓸 수 있도록 활성화(Enable)하고, MSI Message Address/Data 레지스터에 어떤 주소에 어떤 값을 쓰면 인터럽트가 발생하는 지 기록한 뒤 커널로부터 IRQ 번호를 할당받는다.
이후 할당받은 번호를 request_irq() 함수의 첫 번째 인자로 넘겨 IRQ 핸들러를 등록하게 된다.
Q. MSI Capability는 PCIe 구성공간(Configuration Space) 어디에 있으며, 어떻게 찾아갈까?
PCIe 장치는 총 4KB의 Configuration Space를 가지는 데 이 공간은 다음과 같이 세 부분으로 나눌 수 있다.
- 헤더 공간 (00h~3Fh, 첫 64 Byte) : 시스템(Root Complex)이 장치를 식별하고 기본 설정을 하는 공간
- PCI 호환 공간 (40h~FFh, 64~255 Byte 영역) : PCI 레거시 장치와의 하위 호환성을 위해 동일하게 유지되는 공간으로, PCI 표준 Capability가 배치되는 공간
- PCIe 확장 공간 (100h~FFFh, 256~4015 Byte 영역) : PCIe로 오면서 추가된 고급 기능(AER, 전력 관리 등)을 위한 Extended Capability가 배치되는 공간
PCI 장치의 어느 Capability(기능)가 어느 주소에 배치되는지는 사전에 정해진 규칙이 없고, RC가 순회 탐색하여 각 기능을 확인하고 설정할 수 있도록 링크드 리스트로 구현되어 있다. 해당 링크드리스트의 진입점은 헤더 공간 34h 오프셋의 Capability Pointer인데, 이 포인터가 가리키는 오프셋 주소로 점프하면 첫번째 Capability 블록에 도달한다.
각 Capability 블록의 첫 2바이트는 [ Next Capability Pointer | Capability ID ] 구조를 가지며, RC는 Capability ID를 읽어 현재 찾는 기능이 맞는지 확인하고, 일치하지 않으면 뒤의 Next Pointer를 참조해 다음 Capability 블록 오프셋으로 점프하는 식으로 리스트를 순회하다가, Next Pointer 값이 00h이면 리스트의 끝으로 판단하고 탐색을 종료한다.
그리고 Capability ID가 05h면 해당 블록이 MSI Capability임을 의미한다. 그 구조는 아래 첨부한 사진과 같고, 주요 필드를 설명하면 다음과 같다.
- Message Control : MSI Enable 비트, Multiple Message Capable/Enable 활성화할 벡터 설정 필드 등
- Message Address : 장치가 인터럽트를 트리거할 때 MSI 패킷(Memory Write Transaction)을 전송할 대상 CPU 메모리 주소
- Message Data : 인터럽트 발생 시 위 주소로 Write할 기준 데이터 값. 여러 MSI 벡터를 사용하는 경우, 장치가 실제 MSI 패킷 전송 시 이 값의 하위 비트를 변형해 각 벡터를 구분한다.

✓ MSI Capability 구조와 MSI (Memory Write) Transaction

✓ Message Control 필드

4. IRQ 핸들러
4.1. MSI 방식
기존 INTx 방식은 PCI 슬롯의 INT 핀 신호가 CPU에 전달되는 과정에서 I/O APIC의 같은 입력 라인으로 여러 장치 신호가 공유되는 구조이므로, 드라이버는 해당 인터럽트가 내 장치로부터 발생한 것인지 직접 확인해야 한다. 때문에 v1 드라이버 IRQ 핸들러에선 REG_STATUS를 읽어서 Done 비트를 확인하고, 내 장치 인터럽트가 아닌 경우 IRQ_NONE을 반환했었다.
반면, MSI는 장치가 Memory Write TLP로 인터럽트를 발생시키는 방식이라 인터럽트 벡터가 장치 전용으로 할당되므로 핸들러가 호출됐다는 것 자체가 내 장치에서 온 것을 보장한다.
또한 INTx처럼 레벨 트리거가 아닌 엣지 트리거이기 때문에 장치가 물리 핀을 Deassert하도록 ACK할 필요도 없다.
4.2. CQ 순회하여 완료 정보 회수
v2는 v1과 달리 인터럽트 한 번으로 여러 완료 엔트리가 쌓여 있을 수 있기 때문에 CQ를 순회하며 작업 완료 정보를 한꺼번에 수집한다.
이 때, 새 완료 엔트리인지 판별하는 기준은 phase 변수다. CQ는 링 버퍼라 한 바퀴 돌고 나면 슬롯을 재사용하는데, 장치는 첫 바퀴에 phase=1, 그 다음 바퀴엔 phase=0식으로 바꿔가며 기록한다. CQ 버퍼는 0으로 초기화되기 때문에 드라이버는 phase=1이면 새로 쓰인 엔트리라는 것을 알 수 있고, 한 바퀴 돌면 드라이버도 expected_phase를 반전시켜 그 다음부턴 phase=0인 엔트리를 새 것이구나 하고 알 수 있다.
드라이버 입장에선 REG_CQ_TAIL 레지스터를 MMIO로 읽어서 tail 포인터 위치를 알면 슬롯 몇개를 읽어야하는지 알 수 있긴 하지만, 굳이 여기서 phase 정보를 사용하는 이유는 MMIO 읽기는 PCIe 트랜잭션을 거치지만 CQ는시스템 RAM을 읽는 거라 훨씬 빠르기 때문이다.
한편, IRQ 핸들러와 DQBUF ioctl에서 done_queue를 공유하기 때문에 spinlock으로 보호했다. spinlock 자체는 멀티코어 환경에서 자원을 보호하고 커널 선점을 방지하지만, 동일한 CPU에서 인터럽트가 발생할 경우 IRQ 핸들러가 재진입하며 데드락이 발생할 수 있다. 이를 방지하기 위해 현재 CPU의 인터럽트를 비활성화하고 상태를 저장한 뒤 락을 획득하는 spin_lock_irqsave()/spin_unlock_irqrestore()를 사용하는 것이 관례이다.
마지막으로는 CQ에서 읽은 완료 정보는 done_queue에 쌓아두고 head 포인터를 증가시킨 뒤 도어벨을 울려 해당 슬롯을 장치가 다시 쓸 수 있게 한 다음, wake_up_interruptible로 poll 대기 중인 앱을 깨운다.
// IRQ 핸들러 (MSI 전용)
static irqreturn_t vip_dma_irq_handler(int irq, void *data)
{
struct vip_dev *v = data;
unsigned long flags;
bool wakeup = false;
// IRQ 컨텍스트에서는 sleep 불가이므로 mutex 대신 spinlock 사용
spin_lock_irqsave(&v->cq_lock, flags);
// phase 비트로 새 완료 엔트리를 판별하며 CQ 순회
while (v->cq[v->cq_head].phase == v->expected_phase) {
struct vip_cq_entry *entry = &v->cq[v->cq_head];
// done_queue에 완료 결과 추가 (DQBUF ioctl이 소비할 때까지 보관)
if (v->dq_count < VIP_QUEUE_DEPTH) {
struct vip_dqbuf *d = &v->done_queue[v->dq_tail];
d->job_id = entry->id;
d->out_index = v->jobs[entry->sq_head].out_index;
d->status = entry->status;
v->dq_tail = (v->dq_tail + 1) % VIP_QUEUE_DEPTH;
v->dq_count++;
wakeup = true;
}
// CQ head 포인터 전진 및 한 바퀴 돌면 expected_phase 반전
v->cq_head = (v->cq_head + 1) % VIP_QUEUE_DEPTH;
if (v->cq_head == 0)
v->expected_phase ^= 1;
}
// 처리한 CQ 슬롯을 장치에 반납 (도어벨)
vip_reg_write(v, REG_CQ_HEAD, v->cq_head);
spin_unlock_irqrestore(&v->cq_lock, flags);
if (wakeup)
wake_up_interruptible(&v->wq);
return IRQ_HANDLED;
}
유저 앱
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <poll.h>
#include <errno.h>
#include "vip_dma_uapi.h"
/**
* PIPELINE_DEPTH: 동시에 처리할 프레임 수
* 버퍼 배치: buf[0..PIPE-1] = 입력, buf[PIPE..2*PIPE-1] = 출력
* 슬롯 i -> in_index=i, out_index=i+PIPELINE_DEPTH
*/
#define PIPELINE_DEPTH 16
#define NUM_BUFS (PIPELINE_DEPTH * 2)
static int get_filter_type(const char *name)
{
if (strcasecmp(name, "gray") == 0) return VIP_DMA_FILT_GRAY;
if (strcasecmp(name, "invert") == 0) return VIP_DMA_FILT_INVERT;
if (strcasecmp(name, "bright") == 0) return VIP_DMA_FILT_BRIGHT;
return -1;
}
static void print_usage(const char *prog)
{
printf("Usage: %s <dev> <in.raw> <out.raw> <w> <h> <filter> [param]\n", prog);
printf("\nAvailable Filters:\n");
printf(" gray : Convert to grayscale\n");
printf(" invert : Invert colors\n");
printf(" bright : Adjust brightness (use [param] for intensity, e.g. 100 or -100)\n");
printf("\nExample:\n");
printf(" %s /dev/vipd0 test.raw out.raw 640 480 gray\n", prog);
printf("\n");
}
int main(int argc, char *argv[])
{
if (argc < 7) {
print_usage(argv[0]);
return -1;
}
const char *dev_path = argv[1];
const char *in_path = argv[2];
const char *out_path = argv[3];
uint32_t w = (uint32_t)atoi(argv[4]);
uint32_t h = (uint32_t)atoi(argv[5]);
int32_t param = (argc > 7 ? atoi(argv[7]) : 0);
int filter = get_filter_type(argv[6]);
if (filter < 0) {
fprintf(stderr, "Error: Invalid filter type '%s'\n", argv[6]);
return -1;
}
int ret = 0;
int dev_fd = -1, in_fd = -1, out_fd = -1;
void *buf_maps[NUM_BUFS];
void *in_file_map = MAP_FAILED;
for (int i = 0; i < NUM_BUFS; i++)
buf_maps[i] = MAP_FAILED;
size_t frame_size = (size_t)w * h * 3;
size_t actual_bufsz = 0;
// 1. 장치 파일 열기
dev_fd = open(dev_path, O_RDWR);
if (dev_fd < 0) {
perror("Error: open device failed");
ret = -1;
goto cleanup;
}
// 2. 장치 식별자 확인
uint32_t hw_id = 0;
if (ioctl(dev_fd, VIP_DMA_IOCTL_GET_ID, &hw_id) < 0) {
perror("Error: GET_ID ioctl failed");
ret = -1;
goto cleanup;
}
printf("VIP Hardware ID: 0x%08X\n", hw_id);
if (hw_id != 0xFEE1600D) {
fprintf(stderr, "Error: VIP ID mismatch (read: 0x%08X)\n", hw_id);
ret = -1;
goto cleanup;
}
// 3. DMA 버퍼 풀 할당 (입력 PIPELINE_DEPTH개 + 출력 PIPELINE_DEPTH개)
struct vip_reqbufs req = {
.count = NUM_BUFS,
.buf_size = (uint32_t)frame_size,
};
if (ioctl(dev_fd, VIP_DMA_IOCTL_REQBUFS, &req) < 0) {
perror("Error: REQBUFS ioctl failed");
ret = -1;
goto cleanup;
}
actual_bufsz = req.buf_size;
// 4. 전체 DMA 버퍼를 유저 공간에 매핑
for (int i = 0; i < NUM_BUFS; i++) {
buf_maps[i] = mmap(NULL, actual_bufsz, PROT_READ | PROT_WRITE,
MAP_SHARED, dev_fd, (off_t)i * actual_bufsz);
if (buf_maps[i] == MAP_FAILED) {
fprintf(stderr, "Error: mmap buf[%d] failed: %s\n", i, strerror(errno));
ret = -1;
goto cleanup;
}
}
// 5. 입력 RAW 파일 열기 및 크기 확인
in_fd = open(in_path, O_RDONLY);
if (in_fd < 0) {
perror("Error: open input file failed");
ret = -1;
goto cleanup;
}
struct stat st;
if (fstat(in_fd, &st) < 0) {
perror("Error: fstat failed");
ret = -1;
goto cleanup;
}
size_t total_size = (size_t)st.st_size;
uint32_t total_frames = (uint32_t)(total_size / frame_size);
if (total_frames == 0) {
fprintf(stderr, "Error: input file too small for given resolution\n");
ret = -1;
goto cleanup;
}
printf("Input: %u frames (%ux%u, %.1f MB)\n",
total_frames, w, h, (double)total_size / (1024 * 1024));
in_file_map = mmap(NULL, total_size, PROT_READ, MAP_PRIVATE, in_fd, 0);
if (in_file_map == MAP_FAILED) {
perror("Error: mmap input file failed");
ret = -1;
goto cleanup;
}
// 6. 출력 파일 열기
out_fd = open(out_path, O_RDWR | O_CREAT | O_TRUNC, 0644);
if (out_fd < 0) {
perror("Error: open output file failed");
ret = -1;
goto cleanup;
}
printf("[Starting] Filter: %s, Param: %d, Pipeline: %d\n", argv[6], param, PIPELINE_DEPTH);
// 7. 파이프라인 프레임 루프
struct pollfd pfd = { .fd = dev_fd, .events = POLLIN };
uint32_t next_submit = 0; // 다음 제출할 프레임 번호
uint32_t next_collect = 0; // 다음 회수할 프레임 번호
uint32_t in_flight = 0; // 현재 장치에 제출했지만 아직 처리 완료 응답을 받지 못한 프레임 수
// (1) 최대 PIPELINE_DEPTH개 선행 제출
while (in_flight < PIPELINE_DEPTH && next_submit < total_frames) {
uint32_t slot = next_submit % PIPELINE_DEPTH;
memcpy(buf_maps[slot],
(uint8_t *)in_file_map + (size_t)next_submit * frame_size,
frame_size);
struct vip_qbuf qbuf = {
.in_index = slot,
.out_index = slot + PIPELINE_DEPTH,
.width = w,
.height = h,
.format = VIP_DMA_FMT_RGB24,
.filter = (uint32_t)filter,
.param = param,
.job_id = next_submit,
};
if (ioctl(dev_fd, VIP_DMA_IOCTL_QBUF, &qbuf) < 0) {
perror("Error: QBUF ioctl failed");
ret = -1;
goto cleanup;
}
next_submit++;
in_flight++;
}
// (2) poll 대기 -> 완료 회수 -> 결과 저장 -> 다음 프레임 제출 -> 반복
while (in_flight > 0) {
int poll_ret = poll(&pfd, 1, 5000);
if (poll_ret <= 0) {
fprintf(stderr, "Error: poll timeout or error (collecting frame %u)\n",
next_collect);
ret = -1;
goto cleanup;
}
// 완료된 프레임 모두 회수 (인터럽트 1회에 여러 CQ 엔트리가 쌓일 수 있음)
while (1) {
struct vip_dqbuf dqbuf;
if (ioctl(dev_fd, VIP_DMA_IOCTL_DQBUF, &dqbuf) < 0) {
if (errno == EAGAIN)
break; // 더 이상 완료된 항목 없으면 탈출
perror("Error: DQBUF ioctl failed");
ret = -1;
goto cleanup;
}
if (dqbuf.status != 0) {
fprintf(stderr, "Error: Device reported error on frame %u\n",
dqbuf.job_id);
ret = -1;
goto cleanup;
}
// 결과 저장 (장치가 순서대로 처리하므로 순차 write)
uint32_t out_slot = dqbuf.out_index;
if (write(out_fd, buf_maps[out_slot], frame_size) != (ssize_t)frame_size) {
perror("Error: write output failed");
ret = -1;
goto cleanup;
}
in_flight--;
next_collect++;
if (next_collect % 30 == 0 || next_collect == total_frames)
printf("Progress: %u/%u frames\n", next_collect, total_frames);
// 빈 슬롯에 다음 프레임 즉시 제출
if (next_submit < total_frames) {
uint32_t slot = next_submit % PIPELINE_DEPTH;
memcpy(buf_maps[slot],
(uint8_t *)in_file_map + (size_t)next_submit * frame_size,
frame_size);
struct vip_qbuf qbuf = {
.in_index = slot,
.out_index = slot + PIPELINE_DEPTH,
.width = w,
.height = h,
.format = VIP_DMA_FMT_RGB24,
.filter = (uint32_t)filter,
.param = param,
.job_id = next_submit,
};
if (ioctl(dev_fd, VIP_DMA_IOCTL_QBUF, &qbuf) < 0) {
perror("Error: QBUF ioctl failed");
ret = -1;
goto cleanup;
}
next_submit++;
in_flight++;
}
}
}
printf("Done! Output saved to %s (%u frames)\n", out_path, next_collect);
cleanup:
if (in_file_map != MAP_FAILED) munmap(in_file_map, total_size);
for (int i = NUM_BUFS - 1; i >= 0; i--) {
if (buf_maps[i] != MAP_FAILED)
munmap(buf_maps[i], actual_bufsz);
}
if (out_fd >= 0) close(out_fd);
if (in_fd >= 0) close(in_fd);
if (dev_fd >= 0) close(dev_fd);
return ret;
}
사용법은 v1의 유저앱과 동일하다.
테스트
1. ffmpeg를 이용해 테스트 영상을 준비한다.
- 해상도 : 640x480
- framerate : 30fps
- 영상 길이 : 5초
- 영상 패턴 : testsrc2 컬러바
- 픽셀 포맷 : RGB24
$ ffmpeg -f lavfi -i testsrc2=size=640x480:rate=30 -t 5 \
-f rawvideo -pix_fmt rgb24 host/input_vid.raw
파일 크기는 640 x 480 x 3 bytes x 150 frame = 131.8MB 정도이다.

2. QEMU 실행
# 1. 환경 변수 등록 (본인 환경에 맞게 수정)
QEMU_WORKSPACE=/home/jo/workspace/qemu-workspace
QEMU_BIN=$QEMU_WORKSPACE/qemu/build/qemu-system-arm
KERNEL=$QEMU_WORKSPACE/linux-6.12.65/arch/arm/boot/zImage
INITRD=$QEMU_WORKSPACE/rootfs.cpio.gz
MEM=2048M
MACHINE="virt,highmem=off"
# 2. QEMU 실행
"$QEMU_BIN" \
-M "$MACHINE" \
-m "$MEM" \
-kernel "$KERNEL" \
-initrd "$INITRD" \
-append "console=ttyAMA0" \
-nographic \
-device pci-vip \
-device pci-vip \
-device pci-vip-dma \
-device pci-vip-dma \
-fsdev local,id=hostshare,path=$QEMU_WORKSPACE/vip-workspace,security_model=none \
-device virtio-9p-pci,fsdev=hostshare,mount_tag=hostshare
3. 게스트 OS 내부에서 호스트 공유 디렉토리 마운트 및 드라이버 로드
이 부분은 부팅 시 실행되도록 자동화 스크립트를 만들어 뒀다면 생략 가능하다.
$ mkdir -p /mnt/shared
$ mount -t 9p -o trans=virtio hostshare /mnt/shared
$ insmod /mnt/shared/driver/vip_dma_driver.ko
4. 9p 공유 경로에서 테스트용 영상을 /tmp 경로로 복사해 옴
DMA 버퍼에서 9p 파일시스템으로 바로 복사가 불가능하기 때문에 /tmp(tmpfs)를 경유함
$ mkdir -p /tmp
$ cp /mnt/shared/host/input_vid.raw /tmp/input_vid.raw
5. 테스트
$ cd /mnt/shared/app
$ ./vipctl_dma /dev/vipd0 /tmp/input_vid.raw /tmp/out_gray.raw 640 480 gray
$ ./vipctl_dma /dev/vipd0 /tmp/input_vid.raw /tmp/out_invert.raw 640 480 invert
$ ./vipctl_dma /dev/vipd0 /tmp/input_vid.raw /tmp/out_brighter.raw 640 480 bright 100
$ ./vipctl_dma /dev/vipd0 /tmp/input_vid.raw /tmp/out_darker.raw 640 480 bright -100
6. 테스트 결과를 다시 9p 공유 경로로 복사
$ cp /tmp/out_gray.raw /mnt/shared/host/output_vid_gray.raw
$ cp /tmp/out_invert.raw /mnt/shared/host/output_vid_invert.raw
$ cp /tmp/out_brighter.raw /mnt/shared/host/output_vid_brighter.raw
$ cp /tmp/out_darker.raw /mnt/shared/host/output_vid_darker.raw
7. 이후 호스트에서 결과 확인
$ ffplay -f rawvideo -pixel_format rgb24 -video_size 640x480 -framerate 30 host/output_vid_gray.raw
$ ffplay -f rawvideo -pixel_format rgb24 -video_size 640x480 -framerate 30 host/output_vid_invert.raw
$ ffplay -f rawvideo -pixel_format rgb24 -video_size 640x480 -framerate 30 host/output_vid_brighter.raw
$ ffplay -f rawvideo -pixel_format rgb24 -video_size 640x480 -framerate 30 host/output_vid_darker.raw
작업 처리 결과




마지막으로 동작 흐름을 정리해보면 다음과 같다.
유저앱 ↔ 드라이버 ↔ 장치 간 동작 흐름 (초기화 과정)

유저앱 ↔ 드라이버 ↔ 장치 간 동작 흐름 (프레임 처리 루프)

'임베디드 개발 > 임베디드 리눅스' 카테고리의 다른 글
| Linux PCIe Driver 학습 프로젝트 (0) | 2026.06.21 |
|---|---|
| QEMU 기반 커스텀 PCIe 장치 에뮬레이션 - (4) DMA + MSI (0) | 2026.06.14 |
| LDD ] PCIe 디바이스 드라이버 작성하기 - Char with ioctl & mmap (2/2) (0) | 2026.05.03 |
| LDD ] PCIe 디바이스 드라이버 작성하기 - Char with ioctl & mmap (1/2) (0) | 2026.04.26 |
| QEMU 기반 커스텀 PCIe 장치 에뮬레이션 - (3) (0) | 2026.04.19 |