GN⁺: PCI-e 학습: 드라이버 및 DMA
(blog.davidv.dev)PCI-e 학습: 드라이버 및 DMA
이전 항목 요약
- 이전 항목에서는 간단한 PCI-e 장치를 구현하여 수동으로 주소(
0xfe000000
)를 사용해 32비트씩 읽고 쓰는 방법을 다루었음. - 프로그래밍적으로 이 주소를 얻기 위해 PCI 서브시스템에서 메모리 매핑 세부 정보를 요청해야 함.
드라이버 구조체 생성
-
struct pci_driver
를 생성해야 하며, 지원되는 장치 테이블과probe
함수가 필요함. - 지원되는 장치 테이블은 장치/벤더 ID 쌍의 배열로 구성됨.
static struct pci_device_id gpu_id_tbl[] = {
{ PCI_DEVICE(0x1234, 0x1337) },
{ 0, },
};
-
probe
함수는 장치/벤더 ID가 일치할 때 호출되며, 장치의 메모리 영역을 참조하도록 드라이버 상태를 업데이트해야 함.
typedef struct GpuState {
struct pci_dev *pdev;
u8 __iomem *hwmem;
} GpuState;
probe
함수 구현
- 장치를 활성화하고
pci_dev
에 대한 참조를 저장함.
static int gpu_probe(struct pci_dev *pdev, const struct pci_device_id *id) {
int bars;
unsigned long mmio_start, mmio_len;
GpuState* gpu = kmalloc(sizeof(struct GpuState), GFP_KERNEL);
gpu->pdev = pdev;
pci_enable_device_mem(pdev);
bars = pci_select_bars(pdev, IORESOURCE_MEM);
pci_request_region(pdev, bars, "gpu-pci");
mmio_start = pci_resource_start(pdev, 0);
mmio_len = pci_resource_len(pdev, 0);
gpu->hwmem = ioremap(mmio_start, mmio_len);
return 0;
}
사용자 공간에 카드 노출
- 이제 커널 드라이버에서 BAR0 주소 공간을 매핑했으므로, 사용자 공간 애플리케이션이 파일 작업을 통해 PCIe 장치와 상호 작용할 수 있도록 문자 장치를 생성할 수 있음.
-
open
,read
,write
함수를 구현해야 함.
static int gpu_open(struct inode *inode, struct file *file);
static ssize_t gpu_read(struct file *file, char __user *buf, size_t count, loff_t *offset);
static ssize_t gpu_write(struct file *file, const char __user *buf, size_t count, loff_t *offset);
DMA 사용
- CPU가 한 번에 하나의 DWORD 데이터를 복사하는 대신, DMA를 사용하여 카드가 데이터를 자체적으로 복사하도록 할 수 있음.
- DMA "함수 호출" 인터페이스 정의:
- CPU가 카드에 복사할 데이터(소스 주소, 길이), 대상 주소, 데이터 흐름 방향(읽기 또는 쓰기)을 알려줌.
- CPU가 카드에 복사를 시작할 준비가 되었음을 알림.
- 카드가 CPU에 전송이 완료되었음을 알림.
#define REG_DMA_DIR 0
#define REG_DMA_ADDR_SRC 1
#define REG_DMA_ADDR_DST 2
#define REG_DMA_LEN 3
#define CMD_ADDR_BASE 0xf00
#define CMD_DMA_START (CMD_ADDR_BASE + 0)
static void write_reg(GpuState* gpu, u32 val, u32 reg) {
iowrite32(val, gpu->hwmem + (reg * sizeof(u32)));
}
void execute_dma(GpuState* gpu, u8 dir, u32 src, u32 dst, u32 len) {
write_reg(gpu, dir, REG_DMA_DIR);
write_reg(gpu, src, REG_DMA_ADDR_SRC);
write_reg(gpu, dst, REG_DMA_ADDR_DST);
write_reg(gpu, len, REG_DMA_LEN);
write_reg(gpu, 1, CMD_DMA_START);
}
MSI-X 설정
- DMA 실행이 비동기적이므로
write
가 완료될 때까지 블록하는 것이 더 나음. - PCI-e 카드는 메시지 신호 인터럽트(MSI)를 통해 CPU에 신호를 보낼 수 있음.
- MSI-X를 설정하려면 각 인터럽트에 대한 구성 공간(MSI-X 테이블)과 대기 중인 인터럽트의 비트맵(PBA)을 저장할 공간을 할당해야 함.
#define IRQ_COUNT 1
#define IRQ_DMA_DONE_NR 0
#define MSIX_ADDR_BASE 0x1000
#define PBA_ADDR_BASE 0x3000
static irqreturn_t irq_handler(int irq, void *data) {
pr_info("IRQ %d received\n", irq);
return IRQ_HANDLED;
}
static int setup_msi(GpuState* gpu) {
int msi_vecs;
int irq_num;
msi_vecs = pci_alloc_irq_vectors(gpu->pdev, IRQ_COUNT, IRQ_COUNT, PCI_IRQ_MSIX | PCI_IRQ_MSI);
irq_num = pci_irq_vector(gpu->pdev, IRQ_DMA_DONE_NR);
request_threaded_irq(irq_num, irq_handler, NULL, 0, "GPU-Dma0", gpu);
return 0;
}
실제로 블록하는 쓰기
- 인터럽트 메커니즘을 사용하여
write
를 블록하도록 대기열을 사용할 수 있음.
wait_queue_head_t wq;
volatile int irq_fired = 0;
static irqreturn_t irq_handler(int irq, void *data) {
irq_fired = 1;
wake_up_interruptible(&wq);
return IRQ_HANDLED;
}
static ssize_t gpu_fb_write(struct file *file, const char __user *buf, size_t count, loff_t *offset) {
GpuState *gpu = (GpuState*) file->private_data;
dma_addr_t dma_addr;
u8* kbuf = kmalloc(count, GFP_KERNEL);
copy_from_user(kbuf, buf, count);
dma_addr = dma_map_single(&gpu->pdev->dev, kbuf, count, DMA_TO_DEVICE);
execute_dma(gpu, DIR_HOST_TO_GPU, dma_addr, *offset, count);
if (wait_event_interruptible(wq, irq_fired != 0)) {
pr_info("interrupted");
return -ERESTARTSYS;
}
kfree(kbuf);
return count;
}
화면에 표시
- 이제 사용자 공간에서
write(2)
를 통해 데이터를 PCI-e 장치로 전달할 수 있는 '프레임버퍼'가 있음. - QEMU의 콘솔 출력에 카드의 버퍼를 연결하여 작동하는 GPU처럼 보이도록 할 수 있음.
struct GpuState {
PCIDevice pdev;
MemoryRegion mem;
QemuConsole* con;
uint32_t registers[0x100000 / 32];
uint32_t framebuffer[0x200000];
};
static void pci_gpu_realize(PCIDevice *pdev, Error **errp) {
gpu->con = graphic_console_init(DEVICE(pdev), 0, &ghwops, gpu);
DisplaySurface *surface = qemu_console_surface(gpu->con);
for(int i = 0; i<640*480; i++) {
((uint32_t*)surface_data(surface))[i] = i;
}
}
static void vga_update_display(void *opaque) {
GpuState* gpu = opaque;
DisplaySurface *surface = qemu_console_surface(gpu->con);
for(int i = 0; i<640*480; i++) {
((uint32_t*)surface_data(surface))[i] = gpu->framebuffer[i % 0x200000 ];
}
dpy_gfx_update(gpu->con, 0, 0, 640, 480);
}
static const GraphicHwOps ghwops = {
.gfx_update = vga_update_display,
};
GN⁺의 정리
- 이 글은 PCI-e 장치 드라이버와 DMA를 다루며, 커널 드라이버를 통해 사용자 공간 애플리케이션이 PCIe 장치와 상호 작용할 수 있도록 하는 방법을 설명함.
- DMA를 사용하여 CPU의 부하를 줄이고 데이터 전송 속도를 높이는 방법을 다룸.
- MSI-X를 사용하여 DMA 전송 완료 시 CPU에 신호를 보내는 방법을 설명함.
- QEMU를 사용하여 가상 환경에서 GPU를 시뮬레이션하고 테스트하는 방법을 다룸.
- 비슷한 기능을 가진 프로젝트로는
pciemu
와Linux Kernel Labs - Device Drivers
가 있음.
Hacker News 의견
-
최종 목표는 FPGA를 사용하여 디스플레이 어댑터를 만드는 것임
- Tang Mega 138k를 사용하여 시작했지만 문서가 많지 않아 시간이 걸리고 있음
- PCI-e 하드 IP가 있는 다른 저렴한 FPGA 보드 추천을 원함
-
이 기사들의 흐름이 매우 마음에 듦
- 충분한 코드로 요점을 설명하고 점진적으로 빌드하는 방식이 좋음
- 새로운 PCI 장치를 만들고 싶어지게 하는 좋은 기술 글쓰기의 예시임
-
Linux PCIe 디바이스 드라이버에 대한 훌륭한 입문서처럼 보임
- Linux 디바이스 드라이버는 작업해본 적 없지만 다른 운영 체제에서 여러 PCIe 드라이버를 작업한 경험이 있음
- 개념이 매우 익숙하게 느껴짐
- 이런 유형의 콘텐츠가 더 많아지길 바람
-
이 글을 작성해줘서 정말 고마움
- 매우 유익하고 실용적임
- 이 분야에서 이런 정보는 정말 드물음
- 프로젝트를 위한 개발/플레이테스트 환경을 만드는 데 필요한 정보를 제공함
- 다른 두 부분도 매우 실용적임
- bootsvc 드라이버 사용법, 버스 마스터링, msi-x 등 많은 유용한 세부 정보가 포함됨