1P by neo 4달전 | favorite | 댓글 1개

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 "함수 호출" 인터페이스 정의:
    1. CPU가 카드에 복사할 데이터(소스 주소, 길이), 대상 주소, 데이터 흐름 방향(읽기 또는 쓰기)을 알려줌.
    2. CPU가 카드에 복사를 시작할 준비가 되었음을 알림.
    3. 카드가 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를 시뮬레이션하고 테스트하는 방법을 다룸.
  • 비슷한 기능을 가진 프로젝트로는 pciemuLinux Kernel Labs - Device Drivers가 있음.
Hacker News 의견
  • 최종 목표는 FPGA를 사용하여 디스플레이 어댑터를 만드는 것임

    • Tang Mega 138k를 사용하여 시작했지만 문서가 많지 않아 시간이 걸리고 있음
    • PCI-e 하드 IP가 있는 다른 저렴한 FPGA 보드 추천을 원함
  • 이 기사들의 흐름이 매우 마음에 듦

    • 충분한 코드로 요점을 설명하고 점진적으로 빌드하는 방식이 좋음
    • 새로운 PCI 장치를 만들고 싶어지게 하는 좋은 기술 글쓰기의 예시임
  • Linux PCIe 디바이스 드라이버에 대한 훌륭한 입문서처럼 보임

    • Linux 디바이스 드라이버는 작업해본 적 없지만 다른 운영 체제에서 여러 PCIe 드라이버를 작업한 경험이 있음
    • 개념이 매우 익숙하게 느껴짐
    • 이런 유형의 콘텐츠가 더 많아지길 바람
  • 이 글을 작성해줘서 정말 고마움

    • 매우 유익하고 실용적임
    • 이 분야에서 이런 정보는 정말 드물음
    • 프로젝트를 위한 개발/플레이테스트 환경을 만드는 데 필요한 정보를 제공함
    • 다른 두 부분도 매우 실용적임
      • bootsvc 드라이버 사용법, 버스 마스터링, msi-x 등 많은 유용한 세부 정보가 포함됨