1 पॉइंट द्वारा GN⁺ 2024-07-29 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • हार्डकोड किए गए BAR0 address पर सीधे peek/poke करने वाले चरण से आगे बढ़कर, Linux PCI subsystem के जरिए BAR memory खोजी जाती है और kernel driver device को initialize करता है
  • ड्राइवर struct pci_driver की ID table और probe function से शुरू होता है, BAR0 को kernel virtual address में map करने के बाद user space access के लिए तैयारी करता है
  • /dev/gpu-io character device के जरिए read(2) और write(2) को जोड़ा जाता है, और container_of से file operations में driver state वापस प्राप्त की जाती है
  • DWORD unit copy में 1.2MiB transfer के लिए लगभग 800ms लगे, लेकिन MMIO register-आधारित DMA call पर बदलने से यह लगभग 300µs तक घट गया
  • DMA completion का इंतज़ार MSI-X interrupt और wait queue से किया जाता है, और अंत में यह QEMU console में framebuffer contents दिखाने वाले fake GPU की तरह काम करता है

Kernel driver में BAR0 ढूंढना और map करना

  • पिछला implementation lspci से कॉपी किए गए BAR0 address 0xfe000000 पर 32-bit unit में सीधे read और write करता था
  • address को हार्डकोड न करने के लिए Linux PCI subsystem से device की memory mapping जानकारी ली जाती है
  • struct pci_driver में दो मुख्य fields चाहिए
    • supported device/vendor ID pairs की table
    • ID match होने पर call किया जाने वाला probe function
  • example device PCI_DEVICE(0x1234, 0x1337) से match होता है
  • driver state GpuState में struct pci_dev *pdev और BAR memory के लिए u8 __iomem * hwmem store किया जाता है
  • probe function निम्न क्रम में device तैयार करता है
    • pci_enable_device_mem(pdev) से device memory access enable करना
    • pci_select_bars(pdev, IORESOURCE_MEM) से usable memory BAR bitfield प्राप्त करना
    • pci_request_region(pdev, bars, "gpu-pci") से BAR address space ownership request करना
    • pci_resource_start(pdev, 0) और pci_resource_len(pdev, 0) से BAR0 का start address और length लेना
    • ioremap(mmio_start, mmio_len) से physical address को kernel virtual address में map करना
  • module_init में pci_register_driver call करने पर boot log में mmio starts at 0xfe000000 और kernel virtual address print होता है

User space में character device के रूप में expose करना

  • BAR0 address space को kernel driver में map करने के बाद, user space program को read(2) और write(2) से PCIe device के साथ interact करने देने के लिए character device बनाया जाता है
  • इस driver को केवल open, read, write तीन file operations चाहिए
  • GpuState में struct cdev cdev जोड़कर setup_chardev में ये काम किए जाते हैं
    • alloc_chrdev_region से device number allocate करना
    • cdev_init और cdev_add से character device register करना
    • device_create से /dev/gpu-io बनाना
  • init script में /busybox mdev -s जोड़कर /dev/ pseudo filesystem को populate किया जाता है
  • इसके बाद /dev/gpu-io character device के रूप में दिखता है, और उदाहरण में major number 241, minor number 0 दिखाई देता है

container_of से file operations में driver state ढूंढना

  • write implementation में struct file* का private_data open को भरना होता है, लेकिन open को अलग private_data या user_data argument नहीं मिलता
  • struct inode में character device को point करने वाला struct cdev *i_cdev pointer होता है
  • क्योंकि GpuState में struct cdev embed किया गया है, container_of(inode->i_cdev, struct GpuState, cdev) से GpuState pointer वापस पाया जा सकता है
  • gpu_open प्राप्त GpuState को file->private_data में store करता है
  • इसके बाद gpu_read और gpu_write, file->private_data से GpuState निकालकर इस्तेमाल करते हैं
  • शुरुआती read/write एक बार में एक DWORD handle करते हैं
    • gpu_read ioread32(gpu->hwmem + *offset) से पढ़ता है और copy_to_user से user buffer में copy करता है
    • gpu_write user buffer से 4 bytes copy करता है और offset को 4 बढ़ाता है
  • छोटे transfer के लिए यह काम करता है, लेकिन बड़े transfer में CPU को हर packet अलग-अलग process करना पड़ता है, इसलिए यह धीमा है
  • 640×480, 32bpp के बराबर 1.2MiB transfer में लगभग 800ms लगे

MMIO registers से DMA call बनाना

  • CPU द्वारा DWORD unit copy बार-बार करने के बजाय, device को सीधे data copy करने देने के लिए DMA का उपयोग किया जाता है
  • work request memory-mapped IO के जरिए भेजा जाता है
    • कुछ memory addresses ऐसे registers की तरह काम करते हैं जो DMA call के arguments बनते हैं
    • दूसरे addresses ऐसे command की तरह काम करते हैं जो function call execute होने का मतलब देते हैं
  • DMA interface में कुछ values हैं जो CPU को device को बतानी होती हैं
    • copy किए जाने वाले data का source address और length
    • destination address
    • data direction: main memory की ओर या main memory से
    • copy शुरू करने की readiness signal
  • device को CPU को transfer complete होने की सूचना देनी होती है
  • example registers इस तरह define किए गए हैं
    • REG_DMA_DIR
    • REG_DMA_ADDR_SRC
    • REG_DMA_ADDR_DST
    • REG_DMA_LEN
  • CMD_DMA_START को register values भरने की action और actual DMA start को अलग करने वाले command address के रूप में इस्तेमाल किया जाता है
  • kernel driver का execute_dma iowrite32 से direction, source, destination, length लिखता है और अंत में CMD_DMA_START पर 1 लिखता है

QEMU device side पर DMA handling

  • QEMU adapter का MMIO gpu_write पुराने implementation को replace करके DMA registers और commands को handle करता है
  • register region पर write होने पर value gpu->registers[reg] में store की जाती है
  • command region में REG_DMA_START आने पर DMA direction check की जाती है
  • DIR_HOST_TO_GPU direction में pci_dma_read call किया जाता है
    • host address REG_DMA_ADDR_SRC होता है
    • device address gpu->framebuffer + REG_DMA_ADDR_DST होता है
    • length REG_DMA_LEN होती है
  • दूसरी DMA direction को example code में Unimplemented DMA direction के रूप में handle किया गया है
  • kernel driver का gpu_fb_write user data को DMA में देने के लिए यह प्रक्रिया अपनाता है
    • kmalloc(count, GFP_KERNEL) से kernel buffer allocate करना
    • copy_from_user से user data को kernel buffer में copy करना
    • dma_map_single(&gpu->pdev->dev, kbuf, count, DMA_TO_DEVICE) से DMA address बनाना
    • execute_dma(gpu, DIR_HOST_TO_GPU, dma_addr, *offset, count) call करना
    • kfree(kbuf) से buffer free करना
  • इस तरीके से गति इतनी बढ़ी कि example system में लगभग 300µs मापी गई

DMA completion को MSI-X interrupt से बताना

  • DMA execution asynchronous है, इसलिए write complete होने तक उसे block करना अधिक सुविधाजनक हो सकता है
  • PCI-e card Message Signalled Interrupts के जरिए CPU को signal भेज सकता है
  • MSI, dedicated electrical connection का उपयोग करने वाले पारंपरिक interrupts से अलग, bus पर सामान्य message packets के रूप में interrupt भेजता है
  • MSI-X setup के लिए QEMU device में दो regions रखे जाते हैं
    • हर interrupt configuration को store करने वाली MSI-X table
    • pending interrupt bitmap यानी PBA
  • example constants इस प्रकार हैं
    • IRQ_COUNT है 1
    • IRQ_DMA_DONE_NR है 0
    • MSIX_ADDR_BASE है 0x1000
    • PBA_ADDR_BASE है 0x3000
  • QEMU के pci_gpu_realize में msix_init और msix_vector_use call करके MSI-X initialize किया जाता है
  • lspci -vv में MSI-X enabled दिखता है, और vector table BAR0 offset 00001000, PBA BAR0 offset 00003000 पर दिखते हैं
  • pci_dma_read खत्म होने के बाद msix_notify(&gpu->pdev, IRQ_DMA_DONE_NR) call करके interrupt भेजा जाता है

Kernel IRQ handler और bus mastering

  • kernel driver pci_alloc_irq_vectors से MSI-X/MSI vectors allocate करता है और pci_irq_vector से IRQ number प्राप्त करता है
  • request_threaded_irq से GPU-Dma0 handler register किया जाता है
  • boot के बाद /proc/interrupts में उदाहरण की तरह IRQ 24 को PCI-MSIX-0000:00:02.0 और GPU-Dma0 के रूप में दिखाया जाता है
  • शुरुआत में यह काम नहीं करता, क्योंकि card के पास CPU को स्वतंत्र रूप से message भेजने की permission नहीं होती
  • device को CPU intervention के बिना system memory को सीधे manipulate करने देने वाली capability को bus mastering कहा जाता है
  • kernel के gpu_probe में pci_set_master(pdev) call करने पर device को bus master permission मिल जाती है
  • इसके बाद write दो बार call करने पर kernel log में IRQ 24 received दो बार print होता है

wait queue से असली blocking write implement करना

  • interrupt-based notification तैयार होने पर Linux wait queue से write को blocking call में बदला जा सकता है
  • global state के रूप में wait_queue_head_t wq और volatile int irq_fired = 0 रखे जाते हैं
  • IRQ handler ये काम करता है
    • irq_fired = 1 से completion state set करना
    • wake_up_interruptible(&wq) से waiting thread को जगाना
    • IRQ_HANDLED return करना
  • setup_msi में init_waitqueue_head(&wq) जोड़ा जाता है
  • gpu_fb_write DMA execute करने के बाद wait_event_interruptible(wq, irq_fired != 0) से interrupt का इंतज़ार करता है
  • wait के दौरान interrupt होने पर -ERESTARTSYS return किया जाता है

QEMU console में framebuffer दिखाना

  • अब user space के write(2) को लेकर DMA से PCI-e device तक भेजने वाला framebuffer मौजूद है, इसलिए इसे QEMU console output से जोड़कर काम करने वाले GPU की तरह दिखाया जाता है
  • QEMU के GpuState में QemuConsole* con जोड़ा जाता है
  • pci_gpu_realize में graphic_console_init से console बनाया जाता है, और qemu_console_surface से display surface लिया जाता है
  • शुरुआती test pattern को 640×480 range की surface data में values भरकर दिखाया जाता है
  • vga_update_display gpu->framebuffer contents को QEMU display surface में copy करता है
  • dpy_gfx_update(gpu->con, 0, 0, 640, 480) से 640×480 region refresh किया जाता है
  • इसके बाद underlying device पर pattern लिखने से display बदलती है
  • source code the Github repo में उपलब्ध है

संदर्भ सामग्री

1 टिप्पणियां

 
GN⁺ 2024-07-29
Hacker News की टिप्पणियाँ
  • इस सीरीज़ का अंतिम लक्ष्य FPGA से display adapter बनाना है
    शुरू करने के लिए Tang Mega 138k [0] खरीदा था, लेकिन documentation ज़्यादा नहीं है इसलिए समय लग रहा है
    अगर PCI-e hard IP वाला कोई सस्ता FPGA board सुझा सकें तो अच्छा होगा
    [0]: https://wiki.sipeed.com/hardware/en/tang/tang-mega-138k/mega...
  • Linux PCIe device driver की शुरुआत के लिए यह बहुत अच्छा लगता है
    मैंने खुद Linux device driver के साथ सीधे काम नहीं किया है, लेकिन कुछ साल पहले दूसरे operating system पर कई PCIe driver पर काम किया था, इसलिए concepts बहुत परिचित लगते हैं
    काश इस तरह का content और ज़्यादा हो
  • लेख का flow सच में बहुत पसंद आया
    core point दिखाने जितना ही code रखा गया है, और step-by-step build-up वाला तरीका अच्छा है
    ज़िंदगी भर कभी नया PCI device बनाना नहीं चाहा था, लेकिन अब थोड़ा मन हो रहा है, और शायद यही अच्छी technical writing की असली कसौटी है
  • ऐसा लेख लिखने के लिए बहुत धन्यवाद, यह एक दुर्लभ क्षेत्र में बेहद practical और जानकारी से भरपूर है
    मैं अपने project के लिए development और playtest environment बनाना चाहता था, लेकिन खोजने के लिए keywords तक नहीं पता थे; यह बिल्कुल वही था जिसकी ज़रूरत थी
    बाकी दो भाग भी अच्छे थे, और उनमें boot service driver code को exit के बाद इस्तेमाल करने के तरीके, bus mastering, MSI-X जैसी वास्तविक चीज़ें, और कई छोटी लेकिन उपयोगी details थीं