PCI-e lernen: Treiber und DMA
(blog.davidv.dev)- Statt BAR0 weiterhin direkt per Peek/Poke über eine hartkodierte Adresse anzusprechen, wird der Linux-PCI-Subsystem genutzt, um den BAR-Speicher zu finden, und der Kernel-Treiber initialisiert das Gerät
- Der Treiber beginnt mit der ID-Tabelle von
struct pci_driverund derprobe-Funktion, mappt dann BAR0 auf eine Kernel-Virtual Address und bereitet den Zugriff aus dem User Space vor - Über ein Character Device unter
/dev/gpu-iowerdenread(2)undwrite(2)angebunden, und mitcontainer_ofwird aus den Dateioperationen wieder auf den Treiberzustand zugegriffen - Das Kopieren in DWORD-Schritten brauchte für eine Übertragung von 1.2MiB etwa 800ms, mit einem DMA-Aufruf über MMIO-Register sank das auf etwa 300µs
- Auf den DMA-Abschluss wird per MSI-X-Interrupt und Wait Queue gewartet; am Ende verhält sich das Ganze wie eine Fake-GPU, die den Framebuffer-Inhalt in der QEMU-Konsole anzeigt
BAR0 im Kernel-Treiber finden und mappen
- Die vorherige Implementierung las und schrieb direkt in 32-Bit-Schritten auf die aus
lspcikopierte BAR0-Adresse0xfe000000 - Damit die Adresse nicht hartkodiert werden muss, werden die Memory-Mapping-Informationen des Geräts aus dem Linux-PCI-Subsystem geholt
struct pci_driverbenötigt zwei zentrale Felder- eine Tabelle unterstützter Device-/Vendor-ID-Paare
- eine
probe-Funktion, die bei passender ID aufgerufen wird
- Das Beispielgerät matcht auf
PCI_DEVICE(0x1234, 0x1337) - Im Treiberzustand
GpuStatewerdenstruct pci_dev *pdevundu8 __iomem * hwmemfür den BAR-Speicher abgelegt - Die
probe-Funktion bereitet das Gerät in folgender Reihenfolge vorpci_enable_device_mem(pdev)aktiviert den Zugriff auf den Gerätespeicherpci_select_bars(pdev, IORESOURCE_MEM)liefert die Bitmaske der verfügbaren Memory-BARspci_request_region(pdev, bars, "gpu-pci")fordert den Besitz des BAR-Adressraums anpci_resource_start(pdev, 0)undpci_resource_len(pdev, 0)liefern Startadresse und Länge von BAR0ioremap(mmio_start, mmio_len)mappt die physische Adresse in den virtuellen Kernel-Adressraum
- Wenn
pci_register_driverinmodule_initaufgerufen wird, erscheinen im Boot-Logmmio starts at 0xfe000000und die virtuelle Kernel-Adresse
Als Character Device für den User Space bereitstellen
- Nachdem der BAR0-Adressraum in den Kernel-Treiber gemappt ist, wird ein Character Device erstellt, damit ein User-Space-Programm per
read(2)undwrite(2)mit dem PCIe-Gerät interagieren kann - Für diesen Treiber werden nur drei Dateioperationen benötigt:
open,readundwrite GpuStatewird umstruct cdev cdeverweitert, undsetup_chardevführt Folgendes ausalloc_chrdev_regionweist eine Gerätenummer zucdev_initundcdev_addregistrieren das Character Devicedevice_createerzeugt/dev/gpu-io
- Dem Init-Skript wird
/busybox mdev -shinzugefügt, um das Pseudo-Dateisystem unter/dev/zu füllen - Danach erscheint
/dev/gpu-ioals Character Device, im Beispiel mit Major-Nummer241und Minor-Nummer0
Mit container_of in Dateioperationen den Treiberzustand finden
- In der
write-Implementierung mussprivate_datavonstruct file*durchopengesetzt werden, aberopenbekommt keinen separatenprivate_data- oderuser_data-Parameter struct inodeenthält einen Zeigerstruct cdev *i_cdev, der auf das Character Device zeigt- Da
GpuStateeinstruct cdeveinbettet, lässt sich mitcontainer_of(inode->i_cdev, struct GpuState, cdev)der Zeiger aufGpuStatewiederherstellen gpu_openspeichert den erhaltenenGpuStateinfile->private_data- Anschließend holen
gpu_readundgpu_writedenGpuStateausfile->private_dataund verwenden ihn - Die anfänglichen Implementierungen von
read/writeverarbeiten jeweils nur ein DWORD auf einmalgpu_readliest mitioread32(gpu->hwmem + *offset)und kopiert den Wert mitcopy_to_userin den User-Puffergpu_writekopiert 4 Byte aus dem User-Puffer und erhöht den Offset um 4
- Für kleine Transfers funktioniert das, aber bei großen Übertragungen ist es langsam, weil die CPU weiterhin Paket für Paket abarbeiten muss
- Eine Übertragung von 1.2MiB für 640×480 bei 32bpp dauerte etwa
800ms
Einen DMA-Aufruf über MMIO-Register aufbauen
- Statt die CPU wiederholt DWORD für DWORD kopieren zu lassen, wird DMA verwendet, damit das Gerät die Daten direkt kopiert
- Die Anforderung wird über Memory-Mapped I/O gesendet
- Einige Speicheradressen dienen als Register, die die Argumente des DMA-Aufrufs aufnehmen
- Andere Adressen fungieren als Befehl, der die Ausführung des Funktionsaufrufs auslöst
- Die DMA-Schnittstelle enthält Werte, die die CPU dem Gerät mitteilen muss
- Source-Adresse und Länge der zu kopierenden Daten
- Destination-Adresse
- Datenrichtung: zum Main Memory oder vom Main Memory aus
- ein Signal, dass alles zum Start des Kopiervorgangs bereit ist
- Das Gerät muss der CPU den Abschluss der Übertragung melden
- Die Beispielregister sind wie folgt definiert
REG_DMA_DIRREG_DMA_ADDR_SRCREG_DMA_ADDR_DSTREG_DMA_LEN
CMD_DMA_STARTwird als Befehlsadresse verwendet, um das Befüllen der Registerwerte vom eigentlichen Start des DMA zu trennenexecute_dmaim Kernel-Treiber schreibt mitiowrite32Richtung, Source, Destination und Länge und schreibt am Ende1nachCMD_DMA_START
DMA-Verarbeitung auf der QEMU-Geräteseite
- Das MMIO-
gpu_writedes QEMU-Adapters ersetzt die vorherige Implementierung und verarbeitet nun DMA-Register und Befehle - Schreibzugriffe auf den Registerbereich speichern den Wert in
gpu->registers[reg] - Wenn im Befehlsbereich
REG_DMA_STARTeingeht, wird die DMA-Richtung geprüft - In Richtung
DIR_HOST_TO_GPUwirdpci_dma_readaufgerufen- die Host-Adresse stammt aus
REG_DMA_ADDR_SRC - die Device-Adresse ist
gpu->framebuffer + REG_DMA_ADDR_DST - die Länge ist
REG_DMA_LEN
- die Host-Adresse stammt aus
- Andere DMA-Richtungen werden im Beispielcode als
Unimplemented DMA directionbehandelt gpu_fb_writeim Kernel-Treiber übergibt User-Daten in folgenden Schritten an DMAkmalloc(count, GFP_KERNEL)allokiert einen Kernel-Puffercopy_from_userkopiert die User-Daten in den Kernel-Pufferdma_map_single(&gpu->pdev->dev, kbuf, count, DMA_TO_DEVICE)erzeugt eine DMA-Adresseexecute_dma(gpu, DIR_HOST_TO_GPU, dma_addr, *offset, count)wird aufgerufenkfree(kbuf)gibt den Puffer wieder frei
- Auf dem Beispielsystem wurde diese Variante mit etwa 300µs gemessen und ist damit deutlich schneller
DMA-Abschluss per MSI-X-Interrupt signalisieren
- Die DMA-Ausführung ist asynchron; es ist daher praktischer, wenn
writeblockiert, bis sie abgeschlossen ist - Eine PCI-e-Karte kann die CPU über Message Signalled Interrupts benachrichtigen
- Im Gegensatz zu klassischen Interrupts mit eigener elektrischer Leitung werden MSI als normale Nachrichtenpakete auf dem Bus übertragen
- Für die MSI-X-Konfiguration hat das QEMU-Gerät zwei Bereiche
- eine MSI-X-Tabelle, in der die Konfiguration jedes Interrupts gespeichert wird
- die PBA als Bitmap für ausstehende Interrupts
- Die Beispielkonstanten sind
IRQ_COUNTist1IRQ_DMA_DONE_NRist0MSIX_ADDR_BASEist0x1000PBA_ADDR_BASEist0x3000
- In
pci_gpu_realizevon QEMU werdenmsix_initundmsix_vector_useaufgerufen, um MSI-X zu initialisieren - In
lspci -vvwird MSI-X anschließend als aktiviert angezeigt; die Vector Table liegt bei BAR0-Offset00001000, die PBA bei BAR0-Offset00003000 - Nach
pci_dma_readwird mitmsix_notify(&gpu->pdev, IRQ_DMA_DONE_NR)ein Interrupt ausgelöst
Kernel-IRQ-Handler und Bus Mastering
- Der Kernel-Treiber weist mit
pci_alloc_irq_vectorsMSI-X/MSI-Vektoren zu und holt sich mitpci_irq_vectordie IRQ-Nummer - Mit
request_threaded_irqwird der HandlerGPU-Dma0registriert - Nach dem Booten zeigt
/proc/interruptsim Beispiel IRQ24mitPCI-MSIX-0000:00:02.0undGPU-Dma0 - Anfangs funktioniert das nicht, weil die Karte keine Berechtigung hat, der CPU unabhängig Nachrichten zu senden
- Die Fähigkeit, ohne Eingriff der CPU direkt auf den Systemspeicher zuzugreifen, heißt Bus Mastering
- Wenn in
gpu_probedes Kernelspci_set_master(pdev)aufgerufen wird, erhält das Gerät Bus-Master-Rechte - Danach erscheinen bei zwei
write-Aufrufen im Kernel-Log zweimalIRQ 24 received
Eine echte blockierende write mit Wait Queue implementieren
- Sobald die interruptbasierte Benachrichtigung bereitsteht, kann
writemit einer Linux-Wait Queue zu einem blockierenden Aufruf gemacht werden - Als globaler Zustand werden
wait_queue_head_t wqundvolatile int irq_fired = 0angelegt - Der IRQ-Handler erledigt Folgendes
irq_fired = 1setzt den Abschlussstatuswake_up_interruptible(&wq)weckt wartende Threads aufIRQ_HANDLEDwird zurückgegeben
setup_msiergänztinit_waitqueue_head(&wq)- Nach dem DMA-Start wartet
gpu_fb_writemitwait_event_interruptible(wq, irq_fired != 0)auf den Interrupt - Wird das Warten unterbrochen, wird
-ERESTARTSYSzurückgegeben
Den Framebuffer in der QEMU-Konsole anzeigen
- Da nun ein Framebuffer existiert, der
write(2)aus dem User Space annimmt und per DMA an das PCI-e-Gerät überträgt, kann er an die Konsolenausgabe von QEMU angebunden werden, damit das Ganze wie eine funktionierende GPU wirkt GpuStatein QEMU wird umQemuConsole* conerweitert- In
pci_gpu_realizewird mitgraphic_console_initeine Konsole erstellt, und mitqemu_console_surfacewird die Display-Surface geholt - Ein initiales Testmuster wird angezeigt, indem Werte in die Surface-Daten des Bereichs 640×480 geschrieben werden
vga_update_displaykopiert den Inhalt vongpu->framebufferin die QEMU-Display-Surfacedpy_gfx_update(gpu->con, 0, 0, 640, 480)aktualisiert den Bereich 640×480- Danach ändert sich die Anzeige, wenn ein Muster auf das zugrunde liegende Device geschrieben wird
- Der Source Code liegt im Github-Repo
1 Kommentare
Hacker-News-Kommentare
Ich habe mir zum Einstieg ein Tang Mega 138k [0] gekauft, aber weil es nicht viel Dokumentation gibt, dauert es etwas.
Wenn jemand ein günstiges FPGA-Board mit PCI-e Hard IP empfehlen kann, wäre ich dankbar.
[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...
Allerdings gibt es als externe High-Speed-Schnittstelle nur einmal USB 3.1 Gen 1.
https://shop.lambdaconcept.com/home/50-screamer-pcie-squirre...
Litefury ist ein Xilinx-Artix-FPGA-Kit im „NVMe-SSD“-Formfaktor (2280 Key M), verwendet einen Xilinx XC7A100T und kostet 102 Euro.
Es gibt nur einige wenige externe High-Speed-LVDS-Ein-/Ausgänge.
https://rhsresearch.com/collections/rhs-public/products/lite...
Vivado ist nach Maßstäben professioneller Software Engineers zwar kein „großartiges“ Tool, gehört bei FPGA-Entwicklung und -Implementierung aber eindeutig zur Branchenspitze.
Auch der Entwicklungspfad für PCIe-Geräte bei Xilinx ist ziemlich gut ausgebaut.
Ich habe zwar noch nie direkt mit Linux-Gerätetreibern gearbeitet, aber vor einigen Jahren mehrere PCIe-Treiber für ein anderes Betriebssystem geschrieben, und die Konzepte kommen mir sehr vertraut vor.
Ich würde gern mehr Inhalte dieser Art sehen.
Es ist nur so viel Code enthalten, wie nötig ist, um den Kern zu zeigen, und alles wird Schritt für Schritt aufgebaut.
Ich hatte mein ganzes Leben lang nie den Wunsch, ein neues PCI-Gerät zu bauen, aber jetzt habe ich ein wenig Lust darauf bekommen; ist das nicht so etwas wie die Nagelprobe für gutes technisches Schreiben?
Ich wollte eine Entwicklungs- und Playtesting-Umgebung für ein Projekt bauen, wusste aber nicht einmal, nach welchen Begriffen ich suchen sollte; genau das war der Inhalt, den ich brauchte.
Auch die anderen zwei Teile waren gut, mit vielen praxisnahen Themen wie der Verwendung von Boot-Service-Treibercode nach dem Beenden, Bus Mastering, MSI-X und kleinen, aber nützlichen Details.