1 puan yazan GN⁺ 2024-07-29 | 1 yorum | WhatsApp'ta paylaş

PCI-e öğrenimi: sürücüler ve DMA

Önceki bölüm özeti
  • Önceki bölümde basit bir PCI-e aygıtı gerçeklenmiş ve 0xfe000000 adresi elle kullanılarak 32 bitlik okuma ve yazma işlemlerinin nasıl yapıldığı ele alınmıştı.
  • Bu adresi programatik olarak almak için PCI alt sisteminden bellek eşleme ayrıntılarını istemek gerekir.
Sürücü yapısının oluşturulması
  • struct pci_driver oluşturulmalı; bunun için desteklenen aygıt tablosu ve bir probe işlevi gerekir.
  • Desteklenen aygıt tablosu, aygıt/vendor ID çiftlerinden oluşan bir dizi halinde tanımlanır.
static struct pci_device_id gpu_id_tbl[] = {
  { PCI_DEVICE(0x1234, 0x1337) },
  { 0, },
};
  • probe işlevi, aygıt/vendor ID eşleştiğinde çağrılır ve sürücü durumunu aygıtın bellek alanına başvuracak şekilde güncellemelidir.
typedef struct GpuState {
  struct pci_dev *pdev;
  u8 __iomem *hwmem;
} GpuState;
probe işlevinin gerçeklenmesi
  • Aygıt etkinleştirilir ve pci_dev için bir başvuru saklanır.
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;
}
Kartın kullanıcı alanına açılması
  • Artık çekirdek sürücüsünde BAR0 adres alanı eşlendiğine göre, kullanıcı alanı uygulamalarının dosya işlemleri üzerinden PCIe aygıtıyla etkileşime girebilmesi için bir karakter aygıtı oluşturulabilir.
  • open, read, write işlevleri gerçeklenmelidir.
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 kullanımı
  • CPU'nun veriyi her seferinde tek bir DWORD kopyalaması yerine, DMA kullanılarak kartın veriyi kendi başına kopyalaması sağlanabilir.
  • DMA "işlev çağrısı" arayüzünün tanımı:
    1. CPU, karta kopyalanacak veriyi (kaynak adresi, uzunluk), hedef adresi ve veri akış yönünü (okuma veya yazma) bildirir.
    2. CPU, karta kopyayı başlatmaya hazır olduğunu bildirir.
    3. Kart, CPU'ya aktarımın tamamlandığını bildirir.
#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 yapılandırması
  • DMA yürütmesi eşzamansız olduğu için, write tamamlanana kadar engellenmesi daha iyidir.
  • PCI-e kartı, mesaj işaretli kesmeler (MSI) üzerinden CPU'ya sinyal gönderebilir.
  • MSI-X kurmak için her kesme için yapılandırma alanını (MSI-X tablosu) ve bekleyen kesmelerin bitmap'ini (PBA) saklayacak alanı ayırmak gerekir.
#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;
}
Gerçekte engelleyen yazma
  • Kesmeler mekanizması kullanılarak write çağrısını engellemek için bir bekleme kuyruğu kullanılabilir.
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;
}
Ekranda gösterim
  • Artık kullanıcı alanından write(2) aracılığıyla veriyi PCI-e aygıtına aktarabilen bir "framebuffer" var.
  • Kartın tamponu, çalışan bir GPU gibi görünmesi için QEMU'nun konsol çıkışına bağlanabilir.
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⁺ özeti

  • Bu yazı, PCI-e aygıt sürücüleri ve DMA'yı ele alıyor; çekirdek sürücüsü üzerinden kullanıcı alanı uygulamalarının PCIe aygıtıyla nasıl etkileşebileceğini açıklıyor.
  • DMA kullanarak CPU yükünün nasıl azaltılacağı ve veri aktarım hızının nasıl artırılacağı anlatılıyor.
  • DMA aktarımı tamamlandığında MSI-X kullanarak CPU'ya nasıl sinyal gönderileceği açıklanıyor.
  • QEMU kullanarak sanal ortamda GPU'nun nasıl simüle edilip test edileceği ele alınıyor.
  • Benzer işlevlere sahip projeler arasında pciemu ve Linux Kernel Labs - Device Drivers yer alıyor.

1 yorum

 
GN⁺ 2024-07-29
Hacker News görüşü
  • Nihai hedef, FPGA kullanarak bir ekran bağdaştırıcısı yapmak

    • Tang Mega 138k ile başlamış, ancak dokümantasyon çok fazla olmadığı için zaman alıyor
    • PCI-e hard IP'ye sahip başka uygun fiyatlı FPGA kartları için öneri istiyor
  • Bu yazı dizisinin akışını gerçekten çok beğeniyorum

    • Yeterli kodla ana noktaları açıklaması ve aşamalı olarak ilerlemesi çok iyi
    • İnsana yeni bir PCI aygıtı yapmak isteten, iyi teknik yazarlığın güzel bir örneği
  • Linux PCIe aygıt sürücüleri için harika bir giriş yazısı gibi görünüyor

    • Linux aygıt sürücüleriyle hiç çalışmadım, ama başka işletim sistemlerinde birkaç PCIe sürücüsü üzerinde çalışma deneyimim var
    • Kavramlar bana çok tanıdık geliyor
    • Bu tür içeriklerin daha fazlasını görmeyi umuyorum
  • Bunu yazdığınız için gerçekten teşekkürler

    • Çok bilgilendirici ve pratik
    • Bu alanda bu tür bilgiler gerçekten nadir
    • Bir proje için geliştirme/playtest ortamı kurmakta ihtiyaç duyulan bilgileri sağlıyor
    • Diğer iki bölüm de çok pratik
      • bootsvc sürücüsünün kullanımı, bus mastering, MSI-X gibi birçok faydalı ayrıntı içeriyor