1 poin oleh GN⁺ 2024-07-29 | 1 komentar | Bagikan ke WhatsApp

Pembelajaran PCI-e: Driver dan DMA

Ringkasan bagian sebelumnya
  • Pada bagian sebelumnya, dibuat perangkat PCI-e sederhana dan dibahas cara membaca serta menulis 32-bit secara manual menggunakan alamat (0xfe000000).
  • Untuk mendapatkan alamat ini secara terprogram, detail pemetaan memori harus diminta dari subsistem PCI.
Membuat struct driver
  • Perlu membuat struct pci_driver, serta tabel perangkat yang didukung dan fungsi probe.
  • Tabel perangkat yang didukung terdiri dari array pasangan device/vendor ID.
static struct pci_device_id gpu_id_tbl[] = {
  { PCI_DEVICE(0x1234, 0x1337) },
  { 0, },
};
  • Fungsi probe dipanggil ketika device/vendor ID cocok, dan harus memperbarui status driver agar mereferensikan area memori perangkat.
typedef struct GpuState {
  struct pci_dev *pdev;
  u8 __iomem *hwmem;
} GpuState;
Implementasi fungsi probe
  • Mengaktifkan perangkat dan menyimpan referensi ke 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;
}
Mengekspos kartu ke user space
  • Sekarang karena driver kernel telah memetakan ruang alamat BAR0, perangkat karakter dapat dibuat agar aplikasi user space bisa berinteraksi dengan perangkat PCIe melalui operasi file.
  • Perlu mengimplementasikan fungsi open, read, dan 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);
Menggunakan DMA
  • Alih-alih CPU menyalin data satu DWORD setiap kali, DMA dapat digunakan agar kartu menyalin data sendiri.
  • Definisi antarmuka "pemanggilan fungsi" DMA:
    1. CPU memberi tahu kartu data yang akan disalin (alamat sumber, panjang), alamat tujuan, dan arah aliran data (baca atau tulis).
    2. CPU memberi tahu kartu bahwa ia siap memulai penyalinan.
    3. Kartu memberi tahu CPU bahwa transfer telah selesai.
#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);
}
Menyiapkan MSI-X
  • Karena eksekusi DMA bersifat asinkron, lebih baik memblokir write sampai selesai.
  • Kartu PCI-e dapat mengirim sinyal ke CPU melalui message signaled interrupts (MSI).
  • Untuk menyiapkan MSI-X, ruang harus dialokasikan untuk configuration space (tabel MSI-X) bagi tiap interupsi dan bitmap interupsi yang sedang tertunda (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 yang benar-benar memblokir
  • Dengan mekanisme interupsi, antrean tunggu dapat digunakan untuk memblokir 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;
}
Menampilkan ke layar
  • Sekarang ada sebuah 'framebuffer' yang dapat menerima data dari user space ke perangkat PCI-e melalui write(2).
  • Buffer kartu dapat dihubungkan ke output konsol QEMU agar terlihat seperti GPU yang berfungsi.
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,
};

Ringkasan GN⁺

  • Artikel ini membahas driver perangkat PCI-e dan DMA, serta menjelaskan cara agar aplikasi user space dapat berinteraksi dengan perangkat PCIe melalui driver kernel.
  • Dibahas cara menggunakan DMA untuk mengurangi beban CPU dan meningkatkan kecepatan transfer data.
  • Dijelaskan cara menggunakan MSI-X untuk mengirim sinyal ke CPU saat transfer DMA selesai.
  • Dibahas cara mensimulasikan dan menguji GPU di lingkungan virtual menggunakan QEMU.
  • Proyek dengan fungsi serupa antara lain pciemu dan Linux Kernel Labs - Device Drivers.

1 komentar

 
GN⁺ 2024-07-29
Opini Hacker News
  • Tujuan akhirnya adalah membuat adaptor display menggunakan FPGA

    • Memulai dengan Tang Mega 138k, tetapi karena dokumentasinya tidak banyak, prosesnya memakan waktu
    • Ingin rekomendasi board FPGA murah lain yang memiliki hard IP PCI-e
  • Sangat menyukai alur rangkaian artikel ini

    • Poin-poin utama dijelaskan dengan kode yang cukup, dan pendekatan membangunnya secara bertahap terasa bagus
    • Ini adalah contoh penulisan teknis yang baik yang membuat orang ingin membuat perangkat PCI baru
  • Ini tampak seperti pengantar yang sangat bagus untuk driver perangkat PCIe di Linux

    • Belum pernah mengerjakan driver perangkat Linux, tetapi punya pengalaman mengerjakan beberapa driver PCIe di sistem operasi lain
    • Konsep-konsepnya terasa sangat familier
    • Semoga ada lebih banyak konten seperti ini
  • Terima kasih banyak telah menulis ini

    • Sangat informatif dan praktis
    • Informasi seperti ini benar-benar langka di bidang ini
    • Memberikan informasi yang diperlukan untuk membuat lingkungan pengembangan/playtest untuk proyek
    • Dua bagian lainnya juga sangat praktis
      • Mencakup banyak detail berguna seperti cara menggunakan driver bootsvc, bus mastering, msi-x, dan lain-lain