Aprendizaje de PCI-e: drivers y DMA
(blog.davidv.dev)- Dejando atrás la etapa de hacer
peek/pokedirectamente sobre una dirección BAR0 hardcodeada, se usa el subsistema PCI de Linux para encontrar la memoria BAR y el driver del kernel inicializa el dispositivo - El driver comienza con la tabla de IDs de
struct pci_drivery la funciónprobe; luego mapea BAR0 como una dirección virtual del kernel y prepara el acceso desde espacio de usuario - A través del dispositivo de caracteres
/dev/gpu-io, se conectanread(2)ywrite(2), y concontainer_ofse recupera el estado del driver desde las operaciones de archivo - La copia por DWORD tardaba unos 800 ms para una transferencia de 1.2 MiB, pero al cambiarla por llamadas DMA basadas en registros MMIO bajó hasta alrededor de 300 µs
- La espera de finalización de DMA se maneja con interrupciones MSI-X y una wait queue; finalmente funciona como una GPU falsa que muestra el contenido del framebuffer en la consola de QEMU
Encontrar y mapear BAR0 desde el driver del kernel
- La implementación anterior leía y escribía directamente en unidades de 32 bits sobre la dirección BAR0
0xfe000000, copiada desdelspci - Para no hardcodear la dirección, se obtiene la información de mapeo de memoria del dispositivo desde el subsistema PCI de Linux
struct pci_drivernecesita dos campos clave- Una tabla de pares device/vendor ID compatibles
- Una función
probeque se llama cuando el ID coincide
- El dispositivo de ejemplo coincide con
PCI_DEVICE(0x1234, 0x1337) - El estado del driver,
GpuState, guardastruct pci_dev *pdevyu8 __iomem * hwmempara la memoria BAR - La función
probeprepara el dispositivo en este orden- Habilita el acceso a la memoria del dispositivo con
pci_enable_device_mem(pdev) - Obtiene el bitfield de BAR de memoria disponibles con
pci_select_bars(pdev, IORESOURCE_MEM) - Solicita la propiedad del espacio de direcciones BAR con
pci_request_region(pdev, bars, "gpu-pci") - Obtiene la dirección inicial y la longitud de BAR0 con
pci_resource_start(pdev, 0)ypci_resource_len(pdev, 0) - Mapea la dirección física a una dirección virtual del kernel con
ioremap(mmio_start, mmio_len)
- Habilita el acceso a la memoria del dispositivo con
- Al llamar
pci_register_driverdesdemodule_init, en el log de arranque se imprimenmmio starts at 0xfe000000y la dirección virtual del kernel
Exponerlo al espacio de usuario como dispositivo de caracteres
- Después de mapear el espacio de direcciones BAR0 en el driver del kernel, se crea un dispositivo de caracteres para que un programa de espacio de usuario interactúe con el dispositivo PCIe mediante
read(2)ywrite(2) - Este driver solo necesita tres operaciones de archivo:
open,readywrite - Se agrega
struct cdev cdevaGpuState, y ensetup_chardevse realizan las siguientes tareas- Asignar un número de dispositivo con
alloc_chrdev_region - Registrar el dispositivo de caracteres con
cdev_initycdev_add - Crear
/dev/gpu-iocondevice_create
- Asignar un número de dispositivo con
- Se agrega
/busybox mdev -sal script de init para poblar el seudofs/dev/ - Luego
/dev/gpu-ioaparece como dispositivo de caracteres; en el ejemplo se muestra con número major241y número minor0
Encontrar el estado del driver desde operaciones de archivo con container_of
- En la implementación de
write,private_datadestruct file*debe ser completado poropen, peroopenno recibe un argumento separado deprivate_datani deuser_data struct inodetiene un punterostruct cdev *i_cdevque apunta al dispositivo de caracteres- Como
GpuStateembebestruct cdev, se puede recuperar el puntero aGpuStateconcontainer_of(inode->i_cdev, struct GpuState, cdev) gpu_openguarda elGpuStateobtenido enfile->private_data- Después,
gpu_readygpu_writeextraen y usanGpuStatedesdefile->private_data - Los
read/writeiniciales procesan un DWORD por vezgpu_readlee conioread32(gpu->hwmem + *offset)y copia al búfer de usuario concopy_to_usergpu_writecopia 4 bytes desde el búfer de usuario e incrementa el offset en 4
- Funciona para transferencias pequeñas, pero es lento para transferencias grandes porque la CPU debe seguir procesando paquete por paquete
- Una transferencia de 1.2 MiB, correspondiente a 640×480 a 32 bpp, tarda unos
800ms
Crear llamadas DMA con registros MMIO
- En vez de que la CPU repita copias por DWORD, se usa DMA para que el dispositivo copie los datos directamente
- Las solicitudes de trabajo se envían mediante entrada/salida mapeada en memoria (memory-mapped IO)
- Algunas direcciones de memoria se usan como registros que actúan como argumentos de la llamada DMA
- Otras direcciones se usan como comandos que significan ejecutar la llamada de función
- La interfaz DMA tiene valores que la CPU debe comunicarle al dispositivo
- La dirección source y la longitud de los datos a copiar
- La dirección destination
- La dirección de los datos: hacia main memory o desde main memory
- Una señal de que está listo para iniciar la copia
- El dispositivo debe avisarle a la CPU cuando termine la transferencia
- Los registros de ejemplo se definen así
REG_DMA_DIRREG_DMA_ADDR_SRCREG_DMA_ADDR_DSTREG_DMA_LEN
CMD_DMA_STARTse usa como dirección de comando para distinguir entre la acción de completar valores de registros y el inicio real de DMAexecute_dmadel driver del kernel escribe la dirección, source, destination y longitud coniowrite32, y al final escribe1enCMD_DMA_START
Manejo de DMA del lado del dispositivo QEMU
- El
gpu_writeMMIO del adaptador QEMU reemplaza la implementación anterior y procesa registros y comandos DMA - Las escrituras en el área de registros guardan el valor en
gpu->registers[reg] - Cuando llega
REG_DMA_STARTen el área de comandos, se verifica la dirección de DMA - En la dirección
DIR_HOST_TO_GPU, se llama apci_dma_read- La dirección host es
REG_DMA_ADDR_SRC - La dirección device es
gpu->framebuffer + REG_DMA_ADDR_DST - La longitud es
REG_DMA_LEN
- La dirección host es
- En el código de ejemplo, las otras direcciones DMA se manejan como
Unimplemented DMA direction gpu_fb_writedel driver del kernel pasa los datos de usuario a DMA con el siguiente procedimiento- Asigna un búfer del kernel con
kmalloc(count, GFP_KERNEL) - Copia los datos de usuario al búfer del kernel con
copy_from_user - Crea una dirección DMA con
dma_map_single(&gpu->pdev->dev, kbuf, count, DMA_TO_DEVICE) - Llama a
execute_dma(gpu, DIR_HOST_TO_GPU, dma_addr, *offset, count) - Libera el búfer con
kfree(kbuf)
- Asigna un búfer del kernel con
- Este enfoque se vuelve lo bastante rápido como para medirse en unos 300 µs en el sistema de ejemplo
Notificar la finalización de DMA con interrupciones MSI-X
- Como la ejecución de DMA es asíncrona, es más cómodo hacer que
writebloquee hasta que termine - Una tarjeta PCI-e puede enviar señales a la CPU mediante Message Signalled Interrupts
- A diferencia de las interrupciones clásicas que usan una conexión eléctrica dedicada, MSI entrega la interrupción como un paquete de mensaje normal sobre el bus
- Para configurar MSI-X, el dispositivo QEMU tiene dos áreas
- Una MSI-X table que guarda la configuración de cada interrupción
- Una PBA, que es el pending interrupt bitmap
- Las constantes de ejemplo son las siguientes
IRQ_COUNTes1IRQ_DMA_DONE_NRes0MSIX_ADDR_BASEes0x1000PBA_ADDR_BASEes0x3000
- En
pci_gpu_realizede QEMU se llama amsix_initymsix_vector_usepara inicializar MSI-X - En
lspci -vv, MSI-X aparece activado; la vector table se muestra en el offset00001000de BAR0 y la PBA en el offset00003000de BAR0 - Después de que termina
pci_dma_read, se envía la interrupción llamando amsix_notify(&gpu->pdev, IRQ_DMA_DONE_NR)
Handler IRQ del kernel y bus mastering
- El driver del kernel asigna vectores MSI-X/MSI con
pci_alloc_irq_vectorsy obtiene el número de IRQ conpci_irq_vector - Registra el handler
GPU-Dma0conrequest_threaded_irq - Después del arranque, en
/proc/interruptsel IRQ24aparece comoPCI-MSIX-0000:00:02.0yGPU-Dma0, como en el ejemplo - Al principio no funciona, porque la tarjeta no tiene permiso para enviar mensajes independientemente a la CPU
- La capacidad que permite que un dispositivo manipule directamente la memoria del sistema sin intervención de la CPU es bus mastering
- Si en
gpu_probedel kernel se llama apci_set_master(pdev), se le otorgan permisos de bus master al dispositivo - Después, al llamar dos veces a
write, el log del kernel imprimeIRQ 24 receiveddos veces
Implementar un write bloqueante real con wait queue
- Una vez lista la notificación basada en interrupciones, se puede cambiar
writea una llamada bloqueante usando una wait queue de Linux - Como estado global se tienen
wait_queue_head_t wqyvolatile int irq_fired = 0 - El handler IRQ realiza las siguientes tareas
- Define el estado de finalización con
irq_fired = 1 - Despierta los threads en espera con
wake_up_interruptible(&wq) - Devuelve
IRQ_HANDLED
- Define el estado de finalización con
- En
setup_msise agregainit_waitqueue_head(&wq) - Después de ejecutar DMA,
gpu_fb_writeespera la interrupción conwait_event_interruptible(wq, irq_fired != 0) - Si se interrumpe durante la espera, devuelve
-ERESTARTSYS
Mostrar el framebuffer en la consola de QEMU
- Como ya existe un framebuffer que recibe
write(2)desde espacio de usuario y lo entrega al dispositivo PCI-e mediante DMA, se conecta a la salida de la consola de QEMU para que parezca una GPU funcionando - Se agrega
QemuConsole* conaGpuStatede QEMU - En
pci_gpu_realizese crea la consola congraphic_console_inity se obtiene la display surface conqemu_console_surface - El patrón de prueba inicial se muestra completando valores en los datos de la surface dentro del rango 640×480
vga_update_displaycopia el contenido degpu->framebuffera la display surface de QEMU- Con
dpy_gfx_update(gpu->con, 0, 0, 640, 480)se actualiza el área de 640×480 - Luego, si se escribe un patrón en el underlying device, cambia lo que se muestra
- El código fuente está en the Github repo
1 comentarios
Comentarios de Hacker News
Compré una Tang Mega 138k [0] para empezar, pero como no hay mucha documentación, me está llevando tiempo.
Si alguien conoce una placa FPGA económica con hard IP de PCI-e, sería genial que la recomendara.
[0]: https://wiki.sipeed.com/hardware/en/tang/tang-mega-138k/mega...
Spartan 6 https://www.blackmagicdesign.com/products/decklink/techspecs...
Artix https://www.blackmagicdesign.com/products/decklink/techspecs...
Artix https://www.blackmagicdesign.com/products/decklink/techspecs...
Artix https://www.blackmagicdesign.com/products/decklink/techspecs...
Pero su única interfaz externa de alta velocidad es un USB 3.1 Gen 1.
https://shop.lambdaconcept.com/home/50-screamer-pcie-squirre...
Litefury es un kit FPGA Xilinx Artix en formato “NVMe SSD” (2280 Key M), usa un Xilinx XC7A100T y cuesta 102 euros.
Solo tiene unas pocas entradas/salidas LVDS externas de alta velocidad.
https://rhsresearch.com/collections/rhs-public/products/lite...
Vivado no es una herramienta “excelente” según el estándar de un ingeniero de software profesional, pero para desarrollo e implementación en FPGA sin duda está entre lo mejor de la industria.
La ruta de desarrollo de dispositivos PCIe de Xilinx también está bastante bien encaminada.
Nunca trabajé directamente con drivers de dispositivos en Linux, pero hace unos años trabajé en varios drivers PCIe en otro sistema operativo, y los conceptos se ven muy familiares.
Ojalá haya más contenido de este tipo.
Incluye solo el código suficiente para mostrar lo esencial y lo va construyendo paso a paso.
En mi vida había querido crear un nuevo dispositivo PCI, pero ahora me dieron un poco de ganas, y me pregunto si eso no es como la prueba de fuego de una buena escritura técnica.
Quería crear un entorno de desarrollo y playtesting para un proyecto, pero ni siquiera sabía qué términos buscar, y esto era justo lo que necesitaba.
Las otras 2 partes también fueron buenas, con mucho contenido práctico como cómo usar el código de un driver de servicios de arranque después de salir, bus mastering, MSI-X y pequeños detalles útiles.