1 puntos por GN⁺ 2024-07-29 | 1 comentarios | Compartir por WhatsApp

Aprendiendo PCI-e: drivers y DMA

Resumen del artículo anterior
  • En el artículo anterior se implementó un dispositivo PCI-e simple y se explicó cómo leer y escribir manualmente de 32 bits en 32 bits usando la dirección (0xfe000000).
  • Para obtener esta dirección de forma programática, hay que solicitar al subsistema PCI los detalles del mapeo de memoria.
Creación de la estructura del driver
  • Hay que crear un struct pci_driver, y se necesita una tabla de dispositivos compatibles y una función probe.
  • La tabla de dispositivos compatibles está compuesta por un arreglo de pares de ID de dispositivo/fabricante.
static struct pci_device_id gpu_id_tbl[] = {
  { PCI_DEVICE(0x1234, 0x1337) },
  { 0, },
};
  • La función probe se llama cuando coinciden los ID de dispositivo/fabricante, y debe actualizar el estado del driver para que haga referencia al área de memoria del dispositivo.
typedef struct GpuState {
  struct pci_dev *pdev;
  u8 __iomem *hwmem;
} GpuState;
Implementación de la función probe
  • Se activa el dispositivo y se guarda una referencia a 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;
}
Exponer la tarjeta al espacio de usuario
  • Ahora que el driver del kernel ya mapeó el espacio de direcciones BAR0, se puede crear un dispositivo de caracteres para que una aplicación en espacio de usuario pueda interactuar con el dispositivo PCIe mediante operaciones de archivo.
  • Hay que implementar las funciones open, read y 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);
Uso de DMA
  • En lugar de que la CPU copie un DWORD de datos a la vez, se puede usar DMA para que la tarjeta copie los datos por sí sola.
  • Definición de la interfaz de “llamada de función” de DMA:
    1. La CPU le indica a la tarjeta qué datos copiar (dirección de origen, longitud), la dirección de destino y la dirección del flujo de datos (lectura o escritura).
    2. La CPU le indica a la tarjeta que ya está lista para iniciar la copia.
    3. La tarjeta le indica a la CPU que la transferencia 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);
}
Configuración de MSI-X
  • Como la ejecución de DMA es asíncrona, conviene más bloquear write hasta que se complete.
  • Una tarjeta PCI-e puede enviar una señal a la CPU mediante interrupciones señalizadas por mensajes (MSI).
  • Para configurar MSI-X, hay que asignar espacio para guardar el espacio de configuración de cada interrupción (la tabla MSI-X) y un mapa de bits (PBA) para las interrupciones pendientes.
#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;
}
Una escritura que realmente bloquea
  • Se puede usar una cola de espera con el mecanismo de interrupciones para hacer que write se bloquee.
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;
}
Mostrar en pantalla
  • Ahora hay un “framebuffer” que permite pasar datos desde el espacio de usuario al dispositivo PCI-e mediante write(2).
  • Se puede conectar el búfer de la tarjeta a la salida de consola de QEMU para que parezca una GPU funcional.
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,
};

Resumen de GN⁺

  • Este artículo trata sobre drivers de dispositivos PCI-e y DMA, y explica cómo permitir que aplicaciones en espacio de usuario interactúen con un dispositivo PCIe a través de un driver del kernel.
  • Explica cómo usar DMA para reducir la carga de la CPU y aumentar la velocidad de transferencia de datos.
  • Describe cómo usar MSI-X para enviar una señal a la CPU cuando se completa una transferencia DMA.
  • Explica cómo simular y probar una GPU en un entorno virtual usando QEMU.
  • Proyectos con funciones similares incluyen pciemu y Linux Kernel Labs - Device Drivers.

1 comentarios

 
GN⁺ 2024-07-29
Comentarios de Hacker News
  • El objetivo final es crear un adaptador de pantalla usando un FPGA

    • Empecé usando el Tang Mega 138k, pero como no hay mucha documentación, me está tomando tiempo
    • Quiero recomendaciones de otras placas FPGA económicas que tengan PCI-e hard IP
  • Me gusta mucho el hilo conductor de estos artículos

    • Explican los puntos clave con suficiente código y van construyendo de forma gradual, lo cual está muy bien
    • Es un ejemplo de buena escritura técnica que dan ganas de crear un nuevo dispositivo PCI
  • Parece una excelente introducción a los controladores de dispositivos PCIe en Linux

    • No he trabajado antes con controladores de dispositivos de Linux, pero sí tengo experiencia trabajando con varios controladores PCIe en otros sistemas operativos
    • Los conceptos se sienten muy familiares
    • Ojalá haya más contenido de este tipo
  • Muchas gracias por escribir esto

    • Es muy informativo y práctico
    • Este tipo de información es realmente rara en este campo
    • Proporciona la información necesaria para crear un entorno de desarrollo/pruebas para el proyecto
    • Las otras dos partes también son muy prácticas
      • Incluyen muchos detalles útiles, como cómo usar el controlador bootsvc, bus mastering, MSI-X, etc.