1 poin oleh GN⁺ 2024-07-29 | 1 komentar | Bagikan ke WhatsApp
  • Beralih dari tahap melakukan peek/poke langsung ke alamat BAR0 yang di-hardcode, driver kernel kini menemukan memori BAR melalui subsistem PCI Linux dan menginisialisasi perangkat
  • Driver dimulai dari tabel ID pada struct pci_driver dan fungsi probe; setelah memetakan BAR0 ke alamat virtual kernel, ia menyiapkan akses dari ruang pengguna
  • Menghubungkan read(2) dan write(2) melalui perangkat karakter /dev/gpu-io, lalu memakai container_of untuk mengambil kembali status driver dari operasi file
  • Penyalinan per DWORD memerlukan sekitar 800ms untuk transfer 1,2MiB, tetapi setelah diganti menjadi pemanggilan DMA berbasis register MMIO, turun hingga sekitar 300µs
  • Penantian selesainya DMA ditangani dengan interupsi MSI-X dan wait queue; pada akhirnya bekerja sebagai GPU palsu yang menampilkan isi framebuffer di konsol QEMU

Menemukan dan memetakan BAR0 dari driver kernel

  • Implementasi sebelumnya membaca dan menulis langsung per 32-bit ke alamat BAR0 0xfe000000 yang disalin dari lspci
  • Agar alamat tidak di-hardcode, informasi pemetaan memori perangkat diambil dari subsistem PCI Linux
  • struct pci_driver membutuhkan dua field inti
    • Tabel pasangan device/vendor ID yang didukung
    • Fungsi probe yang dipanggil ketika ID cocok
  • Perangkat contoh cocok dengan PCI_DEVICE(0x1234, 0x1337)
  • Status driver GpuState menyimpan struct pci_dev *pdev dan u8 __iomem * hwmem untuk memori BAR
  • Fungsi probe menyiapkan perangkat dengan urutan berikut
    • Mengaktifkan akses memori perangkat dengan pci_enable_device_mem(pdev)
    • Mendapatkan bitfield BAR memori yang tersedia dengan pci_select_bars(pdev, IORESOURCE_MEM)
    • Meminta kepemilikan ruang alamat BAR dengan pci_request_region(pdev, bars, "gpu-pci")
    • Mendapatkan alamat awal dan panjang BAR0 dengan pci_resource_start(pdev, 0) dan pci_resource_len(pdev, 0)
    • Memetakan alamat fisik ke alamat virtual kernel dengan ioremap(mmio_start, mmio_len)
  • Jika pci_register_driver dipanggil dari module_init, log boot akan mencetak mmio starts at 0xfe000000 dan alamat virtual kernel

Mengeksposnya ke ruang pengguna sebagai perangkat karakter

  • Setelah ruang alamat BAR0 dipetakan ke driver kernel, dibuat perangkat karakter agar program ruang pengguna dapat berinteraksi dengan perangkat PCIe melalui read(2) dan write(2)
  • Driver ini hanya membutuhkan tiga operasi file: open, read, dan write
  • Tambahkan struct cdev cdev ke GpuState, lalu lakukan pekerjaan berikut di setup_chardev
    • Mengalokasikan nomor perangkat dengan alloc_chrdev_region
    • Mendaftarkan perangkat karakter dengan cdev_init dan cdev_add
    • Membuat /dev/gpu-io dengan device_create
  • Tambahkan /busybox mdev -s ke skrip init untuk mengisi pseudo-filesystem /dev/
  • Setelah itu /dev/gpu-io terlihat sebagai perangkat karakter; pada contoh, muncul dengan nomor major 241 dan minor 0

Menemukan status driver dari operasi file dengan container_of

  • Dalam implementasi write, private_data milik struct file* harus diisi oleh open, tetapi open tidak menerima argumen private_data atau user_data terpisah
  • struct inode memiliki pointer struct cdev *i_cdev yang menunjuk ke perangkat karakter
  • Karena GpuState menyematkan struct cdev, pointer GpuState dapat diambil kembali dengan container_of(inode->i_cdev, struct GpuState, cdev)
  • gpu_open menyimpan GpuState yang didapat ke file->private_data
  • Setelah itu gpu_read dan gpu_write mengambil GpuState dari file->private_data dan menggunakannya
  • read/write awal memproses satu DWORD setiap kali
    • gpu_read membaca dengan ioread32(gpu->hwmem + *offset) lalu menyalinnya ke buffer pengguna dengan copy_to_user
    • gpu_write menyalin 4 byte dari buffer pengguna dan menaikkan offset sebesar 4
  • Ini bekerja untuk transfer kecil, tetapi lambat untuk transfer besar karena CPU harus terus memproses paket satu per satu
  • Transfer 1,2MiB yang setara dengan 640×480, 32bpp memerlukan sekitar 800ms

Membuat pemanggilan DMA dengan register MMIO

  • Alih-alih CPU mengulang penyalinan per DWORD, DMA digunakan agar perangkat menyalin data secara langsung
  • Permintaan pekerjaan dikirim dengan metode memory-mapped IO
    • Beberapa alamat memori digunakan seperti register yang berperan sebagai argumen pemanggilan DMA
    • Alamat lain digunakan seperti perintah yang berarti menjalankan pemanggilan fungsi
  • Antarmuka DMA memiliki nilai yang harus diberitahukan CPU kepada perangkat
    • Alamat source data yang akan disalin dan panjangnya
    • Alamat destination
    • Arah data: menuju main memory atau dari main memory
    • Sinyal bahwa penyalinan siap dimulai
  • Perangkat harus memberi tahu CPU bahwa transfer telah selesai
  • Register contoh didefinisikan sebagai berikut
    • REG_DMA_DIR
    • REG_DMA_ADDR_SRC
    • REG_DMA_ADDR_DST
    • REG_DMA_LEN
  • CMD_DMA_START digunakan sebagai alamat perintah yang membedakan operasi mengisi nilai register dari benar-benar memulai DMA
  • execute_dma pada driver kernel menulis arah, source, destination, dan panjang dengan iowrite32, lalu terakhir menulis 1 ke CMD_DMA_START

Pemrosesan DMA di sisi perangkat QEMU

  • MMIO gpu_write pada adaptor QEMU menggantikan implementasi sebelumnya untuk menangani register dan perintah DMA
  • Penulisan ke area register menyimpan nilai ke gpu->registers[reg]
  • Ketika REG_DMA_START masuk di area perintah, arah DMA diperiksa
  • Pada arah DIR_HOST_TO_GPU, pci_dma_read dipanggil
    • Alamat host adalah REG_DMA_ADDR_SRC
    • Alamat device adalah gpu->framebuffer + REG_DMA_ADDR_DST
    • Panjangnya adalah REG_DMA_LEN
  • Arah DMA lain ditangani sebagai Unimplemented DMA direction pada kode contoh
  • gpu_fb_write pada driver kernel meneruskan data pengguna ke DMA dengan prosedur berikut
    • Mengalokasikan buffer kernel dengan kmalloc(count, GFP_KERNEL)
    • Menyalin data pengguna ke buffer kernel dengan copy_from_user
    • Membuat alamat DMA dengan dma_map_single(&gpu->pdev->dev, kbuf, count, DMA_TO_DEVICE)
    • Memanggil execute_dma(gpu, DIR_HOST_TO_GPU, dma_addr, *offset, count)
    • Membebaskan buffer dengan kfree(kbuf)
  • Cara ini menjadi jauh lebih cepat, terukur sekitar 300µs pada sistem contoh

Memberi tahu penyelesaian DMA dengan interupsi MSI-X

  • Karena eksekusi DMA bersifat asinkron, akan lebih praktis jika write dibuat memblokir sampai selesai
  • Kartu PCI-e dapat mengirim sinyal ke CPU dengan Message Signalled Interrupts
  • Berbeda dari interupsi klasik yang memakai koneksi elektrik khusus, MSI mengirimkan interupsi sebagai paket pesan biasa di atas bus
  • Untuk konfigurasi MSI-X, perangkat QEMU memiliki dua area
    • MSI-X table yang menyimpan konfigurasi tiap interupsi
    • PBA, yaitu pending interrupt bitmap
  • Konstanta contoh adalah sebagai berikut
    • IRQ_COUNT adalah 1
    • IRQ_DMA_DONE_NR adalah 0
    • MSIX_ADDR_BASE adalah 0x1000
    • PBA_ADDR_BASE adalah 0x3000
  • Di pci_gpu_realize milik QEMU, msix_init dan msix_vector_use dipanggil untuk menginisialisasi MSI-X
  • Pada lspci -vv, MSI-X terlihat aktif, vector table ditampilkan sebagai offset BAR0 00001000, dan PBA sebagai offset BAR0 00003000
  • Setelah pci_dma_read selesai, interupsi dikirim dengan memanggil msix_notify(&gpu->pdev, IRQ_DMA_DONE_NR)

Handler IRQ kernel dan bus mastering

  • Driver kernel mengalokasikan vektor MSI-X/MSI dengan pci_alloc_irq_vectors dan memperoleh nomor IRQ dengan pci_irq_vector
  • Handler GPU-Dma0 didaftarkan dengan request_threaded_irq
  • Setelah boot, /proc/interrupts menampilkan IRQ 24 sebagai PCI-MSIX-0000:00:02.0 dan GPU-Dma0, seperti pada contoh
  • Awalnya tidak berjalan, karena kartu tidak memiliki izin untuk mengirim pesan secara independen ke CPU
  • Fitur yang memungkinkan perangkat memanipulasi memori sistem secara langsung tanpa intervensi CPU disebut bus mastering
  • Jika pci_set_master(pdev) dipanggil di gpu_probe kernel, perangkat diberi hak bus master
  • Setelah itu, jika write dipanggil dua kali, log kernel akan mencetak IRQ 24 received dua kali

Mengimplementasikan blocking write sungguhan dengan wait queue

  • Setelah notifikasi berbasis interupsi siap, write dapat diubah menjadi panggilan blocking dengan wait queue Linux
  • Sebagai status global, disediakan wait_queue_head_t wq dan volatile int irq_fired = 0
  • Handler IRQ melakukan pekerjaan berikut
    • Menetapkan status selesai dengan irq_fired = 1
    • Membangunkan thread yang sedang menunggu dengan wake_up_interruptible(&wq)
    • Mengembalikan IRQ_HANDLED
  • Tambahkan init_waitqueue_head(&wq) ke setup_msi
  • Setelah menjalankan DMA, gpu_fb_write menunggu interupsi dengan wait_event_interruptible(wq, irq_fired != 0)
  • Jika terinterupsi saat menunggu, ia mengembalikan -ERESTARTSYS

Menampilkan framebuffer di konsol QEMU

  • Karena kini ada framebuffer yang menerima write(2) dari ruang pengguna dan meneruskannya ke perangkat PCI-e lewat DMA, framebuffer tersebut dihubungkan ke output konsol QEMU agar tampak seperti GPU yang berfungsi
  • Tambahkan QemuConsole* con ke GpuState milik QEMU
  • Di pci_gpu_realize, buat konsol dengan graphic_console_init, lalu dapatkan display surface dengan qemu_console_surface
  • Pola uji awal ditampilkan dengan mengisi nilai ke data surface pada rentang 640×480
  • vga_update_display menyalin isi gpu->framebuffer ke display surface QEMU
  • Area 640×480 diperbarui dengan dpy_gfx_update(gpu->con, 0, 0, 640, 480)
  • Setelah itu, jika pola ditulis ke underlying device, tampilan berubah
  • Kode sumber tersedia di the Github repo

Referensi

1 komentar

 
GN⁺ 2024-07-29
Komentar Hacker News
  • Tujuan akhir seri ini adalah membuat adapter display dengan FPGA.
    Saya sudah membeli Tang Mega 138k [0] untuk memulai, tetapi dokumentasinya tidak banyak, jadi butuh waktu.
    Kalau ada rekomendasi board FPGA murah yang punya hard IP PCI-e, akan sangat membantu.
    [0]: https://wiki.sipeed.com/hardware/en/tang/tang-mega-138k/mega...
  • Ini terlihat sangat bagus sebagai pengantar driver perangkat PCIe Linux.
    Saya belum pernah langsung menangani driver perangkat Linux, tetapi beberapa tahun lalu saya pernah mengerjakan beberapa driver PCIe di sistem operasi lain, dan konsepnya terasa sangat familier.
    Saya berharap konten seperti ini semakin banyak.
  • Saya benar-benar suka alur tulisannya.
    Kodenya hanya sebanyak yang diperlukan untuk menunjukkan inti, lalu dibangun tahap demi tahap.
    Seumur hidup saya tidak pernah ingin membuat perangkat PCI baru, tetapi sekarang jadi sedikit ingin membuatnya; rasanya ini semacam uji asam untuk tulisan teknis yang bagus.
  • Terima kasih banyak sudah menulis artikel seperti ini; di bidang yang jarang dibahas, isinya sangat praktis dan kaya informasi.
    Saya ingin membuat lingkungan pengembangan dan playtest untuk proyek, tetapi bahkan tidak tahu kata kunci pencariannya; ini persis yang saya butuhkan.
    Dua bagian lainnya juga bagus, dengan banyak materi praktis seperti cara menggunakan kode driver boot service setelah exit, bus mastering, MSI-X, serta detail-detail kecil tetapi berguna.