1 Punkte von GN⁺ 2024-07-29 | 1 Kommentare | Auf WhatsApp teilen

PCI-e lernen: Treiber und DMA

Zusammenfassung des vorherigen Teils
  • Im vorherigen Teil wurde ein einfaches PCI-e-Gerät implementiert und behandelt, wie man mit einer manuell verwendeten Adresse (0xfe000000) 32-Bit-Werte liest und schreibt.
  • Um diese Adresse programmatisch zu erhalten, muss man vom PCI-Subsystem Details zum Memory-Mapping anfordern.
Erstellen der Treiberstruktur
  • Es muss eine struct pci_driver erstellt werden; dafür werden eine Tabelle unterstützter Geräte und eine probe-Funktion benötigt.
  • Die Tabelle unterstützter Geräte besteht aus einem Array von Geräte-/Vendor-ID-Paaren.
static struct pci_device_id gpu_id_tbl[] = {
  { PCI_DEVICE(0x1234, 0x1337) },
  { 0, },
};
  • Die probe-Funktion wird aufgerufen, wenn Geräte-/Vendor-ID übereinstimmen, und muss den Treiberzustand so aktualisieren, dass er auf den Speicherbereich des Geräts verweist.
typedef struct GpuState {
  struct pci_dev *pdev;
  u8 __iomem *hwmem;
} GpuState;
Implementierung der probe-Funktion
  • Das Gerät wird aktiviert und eine Referenz auf pci_dev gespeichert.
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;
}
Die Karte im User Space verfügbar machen
  • Da der Kernel-Treiber nun den BAR0-Adressraum gemappt hat, kann ein Character Device erstellt werden, damit Anwendungen im User Space über Dateizugriffe mit dem PCIe-Gerät interagieren können.
  • Es müssen open-, read- und write-Funktionen implementiert werden.
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);
Verwendung von DMA
  • Anstatt dass die CPU Daten jeweils ein DWORD nach dem anderen kopiert, kann DMA verwendet werden, damit die Karte die Daten selbst kopiert.
  • Definition einer DMA-„Funktionsaufruf“-Schnittstelle:
    1. Die CPU teilt der Karte die zu kopierenden Daten mit (Quelladresse, Länge), die Zieladresse und die Richtung des Datenflusses (Lesen oder Schreiben).
    2. Die CPU signalisiert der Karte, dass sie den Kopiervorgang starten kann.
    3. Die Karte signalisiert der CPU, dass die Übertragung abgeschlossen ist.
#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 einrichten
  • Da die DMA-Ausführung asynchron ist, ist es besser, write zu blockieren, bis der Vorgang abgeschlossen ist.
  • PCI-e-Karten können der CPU über Message Signaled Interrupts (MSI) Signale senden.
  • Um MSI-X einzurichten, muss Speicher für den Konfigurationsraum jeder Interrupt-Anforderung (MSI-X-Tabelle) und für die Bitmap ausstehender Interrupts (PBA) reserviert werden.
#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;
}
Tatsächlich blockierendes Schreiben
  • Mit dem Interrupt-Mechanismus kann eine Warteschlange verwendet werden, um write blockierend zu machen.
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;
}
Auf dem Bildschirm anzeigen
  • Es gibt nun einen „Framebuffer“, über den Daten aus dem User Space per write(2) an das PCI-e-Gerät übergeben werden können.
  • Durch das Verbinden des Kartenpuffers mit der Konsolenausgabe von QEMU kann es wie eine funktionierende GPU erscheinen.
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,
};

Zusammenfassung von GN⁺

  • Dieser Beitrag behandelt PCI-e-Gerätetreiber und DMA und erklärt, wie Anwendungen im User Space über einen Kernel-Treiber mit einem PCIe-Gerät interagieren können.
  • Es wird gezeigt, wie DMA verwendet werden kann, um die CPU zu entlasten und die Datenübertragung zu beschleunigen.
  • Es wird erklärt, wie mit MSI-X der CPU signalisiert wird, dass eine DMA-Übertragung abgeschlossen ist.
  • Es wird beschrieben, wie man mit QEMU eine GPU in einer virtuellen Umgebung simuliert und testet.
  • Ähnliche Projekte mit vergleichbarer Funktionalität sind pciemu und Linux Kernel Labs - Device Drivers.

1 Kommentare

 
GN⁺ 2024-07-29
Hacker-News-Kommentar
  • Das Endziel ist, mit einem FPGA einen Display-Adapter zu bauen

    • Ich habe mit dem Tang Mega 138k angefangen, aber da es nicht viel Dokumentation gibt, dauert es seine Zeit
    • Ich hätte gern Empfehlungen für andere günstige FPGA-Boards mit PCI-e-Hard-IP
  • Mir gefällt der Aufbau dieser Artikel sehr

    • Die Kernaussagen werden mit ausreichend Code erklärt, und der schrittweise Aufbau ist gelungen
    • Ein gutes Beispiel für technisches Schreiben, das Lust darauf macht, ein neues PCI-Gerät zu bauen
  • Das wirkt wie eine hervorragende Einführung in Linux-PCIe-Gerätetreiber

    • Ich habe zwar noch nicht an Linux-Gerätetreibern gearbeitet, aber ich habe mehrere PCIe-Treiber auf anderen Betriebssystemen entwickelt
    • Die Konzepte kommen mir sehr vertraut vor
    • Ich hoffe, es gibt mehr Inhalte dieser Art
  • Vielen Dank fürs Schreiben dieses Artikels

    • Sehr informativ und praxisnah
    • Solche Informationen sind in diesem Bereich wirklich selten
    • Er liefert die Informationen, die man braucht, um eine Entwicklungs-/Playtest-Umgebung für ein Projekt aufzubauen
    • Auch die beiden anderen Teile sind sehr praxisnah
      • Sie enthalten viele nützliche Details zur Verwendung des bootsvc-Treibers, zu Bus-Mastering, MSI-X und mehr