1 points par GN⁺ 2024-07-29 | 1 commentaires | Partager sur WhatsApp

Apprendre le PCI-e : pilotes et DMA

Résumé de l’article précédent
  • Dans l’article précédent, on a implémenté un périphérique PCI-e simple et vu comment lire et écrire 32 bits à la fois en utilisant manuellement l’adresse (0xfe000000).
  • Pour obtenir cette adresse de manière programmatique, il faut demander au sous-système PCI les détails du mappage mémoire.
Création de la structure du pilote
  • Il faut créer une structure struct pci_driver, avec une table des périphériques pris en charge et une fonction probe.
  • La table des périphériques pris en charge est constituée d’un tableau de paires ID périphérique/fabricant.
static struct pci_device_id gpu_id_tbl[] = {
  { PCI_DEVICE(0x1234, 0x1337) },
  { 0, },
};
  • La fonction probe est appelée quand l’ID périphérique/fabricant correspond, et doit mettre à jour l’état du pilote pour référencer la zone mémoire du périphérique.
typedef struct GpuState {
  struct pci_dev *pdev;
  u8 __iomem *hwmem;
} GpuState;
Implémentation de la fonction probe
  • On active le périphérique et on stocke une référence vers 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;
}
Exposer la carte à l’espace utilisateur
  • Maintenant que le pilote noyau a mappé l’espace d’adressage BAR0, on peut créer un périphérique caractère pour permettre aux applications en espace utilisateur d’interagir avec le périphérique PCIe via des opérations sur fichier.
  • Il faut implémenter les fonctions open, read et 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);
Utilisation du DMA
  • Au lieu que le CPU copie les données un DWORD à la fois, on peut utiliser le DMA pour que la carte effectue elle-même la copie.
  • Définition de l’interface de « l’appel de fonction » DMA :
    1. Le CPU indique à la carte les données à copier (adresse source, longueur), l’adresse de destination et le sens du flux de données (lecture ou écriture).
    2. Le CPU signale à la carte qu’elle peut démarrer la copie.
    3. La carte signale au CPU que le transfert est terminé.
#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);
}
Configuration de MSI-X
  • Comme l’exécution du DMA est asynchrone, il vaut mieux bloquer write jusqu’à sa fin.
  • Une carte PCI-e peut signaler le CPU via des interruptions signalisées par message (MSI).
  • Pour configurer MSI-X, il faut allouer de l’espace pour la zone de configuration de chaque interruption (table MSI-X) et pour stocker le bitmap des interruptions en attente (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;
}
Un write réellement bloquant
  • En utilisant le mécanisme d’interruption, on peut employer une file d’attente pour bloquer 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;
}
Affichage à l’écran
  • On dispose désormais d’un « framebuffer » auquel l’espace utilisateur peut envoyer des données vers le périphérique PCI-e via write(2).
  • On peut relier le tampon de la carte à la sortie console de QEMU pour lui donner l’apparence d’un GPU fonctionnel.
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,
};

Résumé de GN⁺

  • Cet article traite des pilotes de périphériques PCI-e et du DMA, et explique comment permettre à des applications en espace utilisateur d’interagir avec un périphérique PCIe via un pilote noyau.
  • Il montre comment utiliser le DMA pour réduire la charge du CPU et accélérer les transferts de données.
  • Il explique comment utiliser MSI-X pour signaler au CPU la fin d’un transfert DMA.
  • Il aborde la manière de simuler et tester un GPU dans un environnement virtuel à l’aide de QEMU.
  • Parmi les projets offrant des fonctionnalités similaires, on trouve pciemu et Linux Kernel Labs - Device Drivers.

1 commentaires

 
GN⁺ 2024-07-29
Commentaire Hacker News
  • L’objectif final est de créer un adaptateur d’affichage à l’aide d’un FPGA

    • J’ai commencé avec le Tang Mega 138k, mais comme il n’y a pas beaucoup de documentation, cela prend du temps
    • Je cherche des recommandations pour d’autres cartes FPGA abordables avec un hard IP PCI-e
  • J’aime beaucoup la progression de cette série d’articles

    • Les points clés sont expliqués avec suffisamment de code, et l’approche de construction progressive est appréciable
    • C’est un bon exemple d’écriture technique qui donne envie de créer un nouveau périphérique PCI
  • Cela ressemble à une excellente introduction aux pilotes de périphériques PCIe sous Linux

    • Je n’ai jamais travaillé sur des pilotes de périphériques Linux, mais j’ai de l’expérience sur plusieurs pilotes PCIe dans d’autres systèmes d’exploitation
    • Les concepts me paraissent très familiers
    • J’espère voir davantage de contenu de ce type
  • Merci beaucoup d’avoir écrit cet article

    • C’est très instructif et pratique
    • Ce type d’information est vraiment rare dans ce domaine
    • Cela fournit les informations nécessaires pour créer un environnement de développement/test pour le projet
    • Les deux autres parties sont également très pratiques
      • Elles contiennent de nombreux détails utiles, comme l’utilisation du pilote bootsvc, le bus mastering, MSI-X, etc.