1 puan yazan GN⁺ 2024-07-29 | 1 yorum | WhatsApp'ta paylaş
  • Hardcoded BAR0 adresine doğrudan peek/poke yapma aşamasından çıkıp, Linux PCI alt sistemi ile BAR belleğini buluyor ve çekirdek sürücüsü aygıtı başlatıyor
  • Sürücü, struct pci_driver içindeki ID tablosu ve probe fonksiyonuyla başlar; BAR0'ı çekirdek sanal adresine eşledikten sonra kullanıcı alanı erişimini hazırlar
  • /dev/gpu-io karakter aygıtı üzerinden read(2) ve write(2) bağlanır; container_of ile dosya işlemlerinden sürücü durumu geri alınır
  • DWORD birimli kopyalama, 1.2MiB aktarım için yaklaşık 800ms sürdü; ancak MMIO register tabanlı DMA çağrısına geçince yaklaşık 300µs seviyesine düştü
  • DMA tamamlanmasını bekleme MSI-X interrupt ve wait queue ile işlenir; sonunda QEMU konsolunda framebuffer içeriğini gösteren sahte bir GPU gibi çalışır

BAR0'ı çekirdek sürücüsünde bulmak ve eşlemek

  • Önceki uygulama, lspciden kopyalanan BAR0 adresi 0xfe000000 üzerinde 32 bitlik birimlerle doğrudan okuma ve yazma yapıyordu
  • Adresi hardcode etmemek için Linux PCI alt sisteminden aygıtın bellek eşleme bilgileri alınır
  • struct pci_driver için iki temel alan gerekir
    • Desteklenen device/vendor ID çiftlerinin tablosu
    • ID eşleştiğinde çağrılan probe fonksiyonu
  • Örnek aygıt PCI_DEVICE(0x1234, 0x1337) ile eşleşir
  • Sürücü durumu GpuState, struct pci_dev *pdev ve BAR belleği için u8 __iomem * hwmem saklar
  • probe fonksiyonu aygıtı şu sırayla hazırlar
    • pci_enable_device_mem(pdev) ile aygıt belleğine erişimi etkinleştirir
    • pci_select_bars(pdev, IORESOURCE_MEM) ile kullanılabilir bellek BAR bit alanını alır
    • pci_request_region(pdev, bars, "gpu-pci") ile BAR adres alanının sahipliğini ister
    • pci_resource_start(pdev, 0) ve pci_resource_len(pdev, 0) ile BAR0'ın başlangıç adresini ve uzunluğunu alır
    • ioremap(mmio_start, mmio_len) ile fiziksel adresi çekirdek sanal adresine eşler
  • module_init içinde pci_register_driver çağrıldığında boot log'unda mmio starts at 0xfe000000 ve çekirdek sanal adresi yazdırılır

Kullanıcı alanına karakter aygıtı olarak sunmak

  • BAR0 adres alanı çekirdek sürücüsüne eşlendikten sonra, kullanıcı alanı programının read(2) ve write(2) ile PCIe aygıtıyla etkileşmesi için bir karakter aygıtı oluşturulur
  • Bu sürücüde yalnızca üç dosya işlemi gerekir: open, read, write
  • GpuStatee struct cdev cdev eklenir ve setup_chardev içinde şu işlemler yapılır
    • alloc_chrdev_region ile aygıt numarası ayrılır
    • cdev_init ve cdev_add ile karakter aygıtı kaydedilir
    • device_create ile /dev/gpu-io oluşturulur
  • /dev/ sözde dosya sistemini doldurmak için init script'ine /busybox mdev -s eklenir
  • Bundan sonra /dev/gpu-io bir karakter aygıtı olarak görünür; örnekte major numarası 241, minor numarası 0 olarak gösterilir

container_of ile dosya işlemlerinden sürücü durumunu bulmak

  • write uygulamasında struct file* içindeki private_data alanını open doldurmalıdır; ancak open ayrı bir private_data ya da user_data argümanı almaz
  • struct inode içinde karakter aygıtını gösteren struct cdev *i_cdev işaretçisi bulunur
  • GpuState, struct cdevi içine gömdüğü için container_of(inode->i_cdev, struct GpuState, cdev) ile GpuState işaretçisi geri alınabilir
  • gpu_open, elde edilen GpuStatei file->private_data içine kaydeder
  • Daha sonra gpu_read ve gpu_write, file->private_data içinden GpuStatei çıkarıp kullanır
  • İlk read/write bir seferde bir DWORD işler
    • gpu_read, ioread32(gpu->hwmem + *offset) ile okur ve copy_to_user ile kullanıcı tamponuna kopyalar
    • gpu_write, kullanıcı tamponundan 4 bayt kopyalar ve offset'i 4 artırır
  • Küçük aktarımlarda çalışır; ancak CPU sürekli tek tek paket işlemek zorunda kaldığı için büyük aktarımlarda yavaştır
  • 640×480, 32bpp'ye karşılık gelen 1.2MiB aktarım yaklaşık 800ms sürer

MMIO register'larıyla DMA çağrısı oluşturmak

  • CPU'nun DWORD birimli kopyalamayı tekrar etmesi yerine, veriyi aygıtın doğrudan kopyalaması için DMA kullanılır
  • İş isteği memory-mapped IO yöntemiyle gönderilir
    • Bazı bellek adresleri DMA çağrısının argümanları gibi davranan register'lar olarak kullanılır
    • Diğer adresler ise fonksiyon çağrısını çalıştırmak anlamına gelen komutlar gibi kullanılır
  • DMA arayüzünde CPU'nun aygıta bildirmesi gereken değerler vardır
    • Kopyalanacak verinin source adresi ve uzunluğu
    • destination adresi
    • Veri yönü: main memory tarafına veya main memory'den
    • Kopyalamayı başlatmaya hazır olunduğu sinyali
  • Aygıt aktarımın tamamlandığını CPU'ya bildirmelidir
  • Örnek register'lar şöyle tanımlanır
    • REG_DMA_DIR
    • REG_DMA_ADDR_SRC
    • REG_DMA_ADDR_DST
    • REG_DMA_LEN
  • CMD_DMA_START, register değerlerini doldurma işlemiyle gerçek DMA başlatmayı ayıran komut adresi olarak kullanılır
  • Çekirdek sürücüsünün execute_dma fonksiyonu, iowrite32 ile yönü, source'u, destination'ı ve uzunluğu yazar; en sonda CMD_DMA_START adresine 1 yazar

QEMU aygıtı tarafında DMA işleme

  • QEMU adaptörünün MMIO gpu_write fonksiyonu önceki uygulamanın yerini alarak DMA register'larını ve komutları işler
  • Register alanına yazılanlar gpu->registers[reg] içine değer olarak kaydedilir
  • Komut alanında REG_DMA_START geldiğinde DMA yönü kontrol edilir
  • DIR_HOST_TO_GPU yönünde pci_dma_read çağrılır
    • host adresi REG_DMA_ADDR_SRCdir
    • device adresi gpu->framebuffer + REG_DMA_ADDR_DSTdir
    • uzunluk REG_DMA_LENdir
  • Diğer DMA yönleri örnek kodda Unimplemented DMA direction olarak işlenir
  • Çekirdek sürücüsünün gpu_fb_write fonksiyonu kullanıcı verisini DMA'ya şu adımlarla aktarır
    • kmalloc(count, GFP_KERNEL) ile çekirdek tamponu ayırır
    • copy_from_user ile kullanıcı verisini çekirdek tamponuna kopyalar
    • dma_map_single(&gpu->pdev->dev, kbuf, count, DMA_TO_DEVICE) ile DMA adresi oluşturur
    • execute_dma(gpu, DIR_HOST_TO_GPU, dma_addr, *offset, count) çağırır
    • kfree(kbuf) ile tamponu serbest bırakır
  • Bu yöntem örnek sistemde yaklaşık 300µs ölçülecek kadar hızlanır

DMA tamamlanmasını MSI-X interrupt ile bildirmek

  • DMA yürütmesi asenkron olduğundan, writeın tamamlanana kadar bloklanmasını sağlamak daha kullanışlıdır
  • PCI-e kartı CPU'ya Message Signalled Interrupts ile sinyal gönderebilir
  • MSI, özel elektriksel bağlantı kullanan klasik interrupt'lardan farklı olarak interrupt'ı veri yolu üzerindeki normal mesaj paketleriyle iletir
  • MSI-X ayarı için QEMU aygıtında iki alan bulunur
    • Her interrupt ayarını saklayan MSI-X table
    • pending interrupt bitmap'i olan PBA
  • Örnek sabitler şöyledir
    • IRQ_COUNT değeri 1dir
    • IRQ_DMA_DONE_NR değeri 0dır
    • MSIX_ADDR_BASE değeri 0x1000dir
    • PBA_ADDR_BASE değeri 0x3000dir
  • QEMU'nun pci_gpu_realize fonksiyonunda msix_init ve msix_vector_use çağrılarak MSI-X başlatılır
  • lspci -vv çıktısında MSI-X etkin görünür; vector table BAR0 offset 00001000, PBA ise BAR0 offset 00003000 olarak gösterilir
  • pci_dma_read bittikten sonra msix_notify(&gpu->pdev, IRQ_DMA_DONE_NR) çağrılarak interrupt gönderilir

Çekirdek IRQ handler'ı ve bus mastering

  • Çekirdek sürücüsü pci_alloc_irq_vectors ile MSI-X/MSI vektörlerini ayırır ve pci_irq_vector ile IRQ numarasını alır
  • request_threaded_irq ile GPU-Dma0 handler'ı kaydedilir
  • Boot sonrasında /proc/interrupts içinde örnekteki gibi IRQ 24, PCI-MSIX-0000:00:02.0 ve GPU-Dma0 olarak gösterilir
  • Başta çalışmaz; çünkü kartın CPU'dan bağımsız olarak mesaj gönderme yetkisi yoktur
  • Aygıtın CPU müdahalesi olmadan sistem belleğini doğrudan işleyebilmesini sağlayan özellik bus masteringdir
  • Çekirdekteki gpu_probe içinde pci_set_master(pdev) çağrılırsa aygıta bus master yetkisi verilir
  • Bundan sonra write iki kez çağrıldığında çekirdek log'unda IRQ 24 received iki kez yazdırılır

wait queue ile gerçek blocking write uygulamak

  • Interrupt tabanlı bildirim hazır olduğunda Linux wait queue ile write bloklayan bir çağrıya dönüştürülebilir
  • Global durum olarak wait_queue_head_t wq ve volatile int irq_fired = 0 tutulur
  • IRQ handler'ı şu işleri yapar
    • irq_fired = 1 ile tamamlanma durumunu ayarlar
    • wake_up_interruptible(&wq) ile bekleyen thread'i uyandırır
    • IRQ_HANDLED döndürür
  • setup_msi içine init_waitqueue_head(&wq) eklenir
  • gpu_fb_write, DMA yürütüldükten sonra wait_event_interruptible(wq, irq_fired != 0) ile interrupt'ı bekler
  • Bekleme sırasında interrupt edilirse -ERESTARTSYS döndürür

QEMU konsolunda framebuffer göstermek

  • Kullanıcı alanındaki write(2) çağrısını alıp DMA ile PCI-e aygıtına aktaran bir framebuffer oluştuğu için, QEMU konsol çıktısına bağlanarak çalışan bir GPU gibi görünmesi sağlanır
  • QEMU'nun GpuStateine QemuConsole* con eklenir
  • pci_gpu_realize içinde graphic_console_init ile konsol oluşturulur ve qemu_console_surface ile display surface alınır
  • İlk test pattern'i 640×480 aralığındaki surface verisine değerler doldurularak gösterilir
  • vga_update_display, gpu->framebuffer içeriğini QEMU display surface'e kopyalar
  • dpy_gfx_update(gpu->con, 0, 0, 640, 480) ile 640×480 alanı güncellenir
  • Daha sonra underlying device'a pattern yazıldığında görüntü değişir
  • Kaynak kod the Github repo içinde bulunur

Referanslar

1 yorum

 
GN⁺ 2024-07-29
Hacker News yorumları
  • Bu serinin nihai hedefi FPGA ile bir ekran bağdaştırıcısı yapmak.
    Başlamak için Tang Mega 138k [0] aldım, ancak dokümantasyonu fazla olmadığı için zaman alıyor.
    PCI-e hard IP bulunan uygun fiyatlı FPGA kartı önerisi olan varsa duymak isterim.
    [0]: https://wiki.sipeed.com/hardware/en/tang/tang-mega-138k/mega...
  • Linux PCIe aygıt sürücüleri için çok iyi bir başlangıç gibi görünüyor.
    Linux aygıt sürücüleriyle bizzat hiç uğraşmadım, ancak birkaç yıl önce başka bir işletim sisteminde birkaç PCIe sürücüsü üzerinde çalışmıştım ve kavramlar çok tanıdık geliyor.
    Bu tür içeriklerin daha fazla olmasını isterim.
  • Yazının akışını gerçekten çok sevdim.
    Ana fikri gösterecek kadar kod koyup adım adım üzerine inşa etmesi güzel.
    Hayatım boyunca yeni bir PCI aygıtı yapmak istememiştim, ama şimdi biraz yapmak istiyorum; iyi teknik yazının asıl turnusol testi de bu değil mi?
  • Böyle bir yazı yazdığın için gerçekten teşekkürler; nadir bir alanda son derece pratik ve bilgi dolu.
    Projem için bir geliştirme ve playtest ortamı kurmak istiyordum ama ne aratacağımı bile bilmiyordum; tam ihtiyacım olan içerikti.
    Diğer 2 bölüm de iyiydi; boot services sürücü kodunu exit sonrasında kullanma yöntemi, bus mastering, MSI-X gibi pratik konular ve küçük ama yararlı birçok ayrıntı var.