Tìm hiểu PCI-e: driver và DMA
(blog.davidv.dev)- Thay vì tiếp tục trực tiếp peek/poke vào địa chỉ BAR0 được hardcode, bài viết chuyển sang dùng PCI subsystem của Linux để tìm vùng nhớ BAR và để kernel driver khởi tạo thiết bị
- Driver bắt đầu từ bảng ID và hàm
probecủastruct pci_driver, sau đó ánh xạ BAR0 thành địa chỉ ảo của kernel rồi chuẩn bị cho truy cập từ không gian người dùng - Thông qua character device
/dev/gpu-io, driver nối vớiread(2)vàwrite(2), đồng thời dùngcontainer_ofđể lấy lại trạng thái driver từ file operation - Bản sao theo đơn vị DWORD mất khoảng 800ms cho lần truyền 1.2MiB, nhưng khi đổi sang gọi DMA dựa trên thanh ghi MMIO thì giảm xuống còn khoảng 300µs
- Việc chờ DMA hoàn tất được xử lý bằng ngắt MSI-X và wait queue, và cuối cùng hệ thống hoạt động như một GPU giả để hiển thị nội dung framebuffer trên console của QEMU
Tìm và ánh xạ BAR0 trong kernel driver
- Ở bản triển khai trước, tác giả đọc và ghi trực tiếp theo đơn vị 32-bit vào địa chỉ BAR0
0xfe000000được sao chép từlspci - Để không phải hardcode địa chỉ, bài viết lấy thông tin ánh xạ bộ nhớ của thiết bị từ PCI subsystem của Linux
struct pci_drivercần hai trường cốt lõi- bảng các cặp device/vendor ID mà driver hỗ trợ
- hàm
probeđược gọi khi ID khớp
- Thiết bị ví dụ khớp với
PCI_DEVICE(0x1234, 0x1337) - Trạng thái driver
GpuStatelưustruct pci_dev *pdevvàu8 __iomem * hwmemcho vùng nhớ BAR - Hàm
probechuẩn bị thiết bị theo thứ tự saupci_enable_device_mem(pdev)để bật truy cập bộ nhớ thiết bịpci_select_bars(pdev, IORESOURCE_MEM)để lấy bitfield các BAR bộ nhớ khả dụngpci_request_region(pdev, bars, "gpu-pci")để yêu cầu quyền sở hữu không gian địa chỉ BARpci_resource_start(pdev, 0)vàpci_resource_len(pdev, 0)để lấy địa chỉ bắt đầu và độ dài của BAR0ioremap(mmio_start, mmio_len)để ánh xạ địa chỉ vật lý sang địa chỉ ảo của kernel
- Khi gọi
pci_register_drivertrongmodule_init, log khởi động sẽ in rammio starts at 0xfe000000cùng địa chỉ ảo của kernel
Phơi ra cho user space dưới dạng character device
- Sau khi ánh xạ không gian địa chỉ BAR0 vào kernel driver, bài viết tạo một character device để chương trình user space có thể tương tác với thiết bị PCIe qua
read(2)vàwrite(2) - Driver này chỉ cần ba file operation:
open,read,write - Thêm
struct cdev cdevvàoGpuState, rồi trongsetup_chardevthực hiện các bước saualloc_chrdev_regionđể cấp phát số thiết bịcdev_initvàcdev_addđể đăng ký character devicedevice_createđể tạo/dev/gpu-io
- Thêm
/busybox mdev -svào script khởi tạo để điền pseudo-filesystem/dev/ - Sau đó
/dev/gpu-ioxuất hiện như một character device, trong ví dụ có major241, minor0
Dùng container_of để tìm trạng thái driver trong file operation
- Trong phần cài đặt
write,private_datacủastruct file*phải doopenđiền, nhưngopenkhông nhận đối sốprivate_datahayuser_datariêng struct inodecó con trỏstruct cdev *i_cdevtrỏ tới character device- Vì
GpuStatenhúngstruct cdev, có thể lấy lại con trỏGpuStatebằngcontainer_of(inode->i_cdev, struct GpuState, cdev) gpu_openlưuGpuStatelấy được vàofile->private_data- Sau đó
gpu_readvàgpu_writelấyGpuStatetừfile->private_datađể sử dụng read/writeban đầu xử lý mỗi lần một DWORDgpu_readđọc bằngioread32(gpu->hwmem + *offset)rồi sao chép sang buffer người dùng bằngcopy_to_usergpu_writesao chép 4 byte từ buffer người dùng và tăng offset thêm 4
- Cách này hoạt động với truyền nhỏ, nhưng chậm với truyền lớn vì CPU phải liên tục xử lý từng packet
- Một lần truyền 1.2MiB tương ứng 640×480, 32bpp mất khoảng
800ms
Tạo lời gọi DMA bằng thanh ghi MMIO
- Thay vì để CPU lặp lại việc sao chép theo đơn vị DWORD, bài viết dùng DMA để thiết bị tự sao chép dữ liệu
- Yêu cầu công việc được gửi bằng memory-mapped IO
- một số địa chỉ bộ nhớ được dùng như các thanh ghi đóng vai trò tham số cho lời gọi DMA
- các địa chỉ khác được dùng như lệnh mang ý nghĩa thực thi lời gọi hàm
- Giao diện DMA cần những giá trị mà CPU phải báo cho thiết bị
- địa chỉ source và độ dài dữ liệu cần sao chép
- địa chỉ destination
- hướng dữ liệu: về phía main memory hoặc từ main memory
- tín hiệu cho biết đã sẵn sàng bắt đầu sao chép
- Thiết bị cũng phải báo lại cho CPU khi truyền xong
- Các thanh ghi ví dụ được định nghĩa như sau
REG_DMA_DIRREG_DMA_ADDR_SRCREG_DMA_ADDR_DSTREG_DMA_LEN
CMD_DMA_STARTđược dùng như địa chỉ lệnh để tách việc điền giá trị thanh ghi khỏi việc thực sự khởi động DMAexecute_dmatrong kernel driver dùngiowrite32để ghi hướng, source, destination, độ dài, rồi cuối cùng ghi1vàoCMD_DMA_START
Xử lý DMA ở phía thiết bị QEMU
- Hàm MMIO
gpu_writecủa adapter QEMU thay cho cách cũ để xử lý các thanh ghi và lệnh DMA - Ghi vào vùng thanh ghi sẽ lưu giá trị vào
gpu->registers[reg] - Khi vùng lệnh nhận
REG_DMA_START, thiết bị sẽ kiểm tra hướng DMA - Với hướng
DIR_HOST_TO_GPU, nó gọipci_dma_read- địa chỉ host là
REG_DMA_ADDR_SRC - địa chỉ thiết bị là
gpu->framebuffer + REG_DMA_ADDR_DST - độ dài là
REG_DMA_LEN
- địa chỉ host là
- Các hướng DMA khác trong mã ví dụ được xử lý bằng
Unimplemented DMA direction gpu_fb_writetrong kernel driver chuyển dữ liệu người dùng sang DMA theo quy trình saukmalloc(count, GFP_KERNEL)để cấp phát buffer kernelcopy_from_userđể sao chép dữ liệu từ user space vào buffer kerneldma_map_single(&gpu->pdev->dev, kbuf, count, DMA_TO_DEVICE)để tạo địa chỉ DMA- gọi
execute_dma(gpu, DIR_HOST_TO_GPU, dma_addr, *offset, count) kfree(kbuf)để giải phóng buffer
- Cách này tăng tốc rõ rệt, được đo khoảng 300µs trên hệ thống ví dụ
Báo hoàn tất DMA bằng ngắt MSI-X
- Vì DMA chạy bất đồng bộ, sẽ tiện hơn nếu làm cho
writebị block cho đến khi hoàn tất - Card PCI-e có thể gửi tín hiệu cho CPU bằng Message Signalled Interrupts
- Không giống ngắt kiểu cũ dùng đường điện riêng, MSI truyền ngắt dưới dạng packet thông điệp thông thường trên bus
- Để cấu hình MSI-X, thiết bị QEMU có hai vùng
- MSI-X table lưu cấu hình cho từng ngắt
- PBA là bitmap các ngắt pending
- Các hằng số ví dụ như sau
IRQ_COUNTlà1IRQ_DMA_DONE_NRlà0MSIX_ADDR_BASElà0x1000PBA_ADDR_BASElà0x3000
- Trong
pci_gpu_realizecủa QEMU, tác giả gọimsix_initvàmsix_vector_useđể khởi tạo MSI-X - Trong
lspci -vv, MSI-X được hiển thị là đã bật, vector table ở BAR0 offset00001000, còn PBA ở BAR0 offset00003000 - Sau khi
pci_dma_readkết thúc, thiết bị gọimsix_notify(&gpu->pdev, IRQ_DMA_DONE_NR)để phát ngắt
IRQ handler của kernel và bus mastering
- Kernel driver cấp phát vector MSI-X/MSI bằng
pci_alloc_irq_vectorsvà lấy số IRQ bằngpci_irq_vector - Sau đó đăng ký handler
GPU-Dma0bằngrequest_threaded_irq - Sau khi boot,
/proc/interruptssẽ hiển thị ví dụ IRQ24vớiPCI-MSIX-0000:00:02.0vàGPU-Dma0 - Ban đầu nó chưa hoạt động vì card chưa có quyền tự gửi thông điệp đến CPU
- Tính năng cho phép thiết bị trực tiếp thao tác với system memory mà không cần CPU can thiệp được gọi là bus mastering
- Khi gọi
pci_set_master(pdev)tronggpu_probe, thiết bị sẽ được cấp quyền bus master - Sau đó nếu gọi
writehai lần, kernel log sẽ inIRQ 24 receivedhai lần
Dùng wait queue để triển khai blocking write thực sự
- Khi thông báo dựa trên ngắt đã sẵn sàng, có thể dùng wait queue của Linux để biến
writethành lời gọi block - Trạng thái toàn cục gồm
wait_queue_head_t wqvàvolatile int irq_fired = 0 - IRQ handler thực hiện các việc sau
- đặt
irq_fired = 1để đánh dấu hoàn tất - gọi
wake_up_interruptible(&wq)để đánh thức thread đang chờ - trả về
IRQ_HANDLED
- đặt
- Trong
setup_msi, thêminit_waitqueue_head(&wq) gpu_fb_writesau khi chạy DMA sẽ chờ ngắt bằngwait_event_interruptible(wq, irq_fired != 0)- Nếu bị ngắt trong lúc chờ, hàm trả về
-ERESTARTSYS
Hiển thị framebuffer trên console của QEMU
- Sau khi có framebuffer nhận
write(2)từ user space và chuyển vào thiết bị PCI-e bằng DMA, bài viết nối nó với đầu ra console của QEMU để trông như một GPU hoạt động - Thêm
QemuConsole* convàoGpuStatecủa QEMU - Trong
pci_gpu_realize, gọigraphic_console_initđể tạo console vàqemu_console_surfaceđể lấy display surface - Mẫu thử ban đầu hiển thị bằng cách điền giá trị vào dữ liệu surface trong vùng 640×480
vga_update_displaysao chép nội dunggpu->framebuffersang display surface của QEMUdpy_gfx_update(gpu->con, 0, 0, 640, 480)cập nhật vùng 640×480- Từ đó, khi ghi pattern vào thiết bị nền bên dưới thì hình hiển thị cũng thay đổi
- Mã nguồn có tại the Github repo
1 bình luận
Ý kiến trên Hacker News
Tôi đã mua Tang Mega 138k [0] để bắt đầu, nhưng tài liệu không nhiều nên đang mất khá nhiều thời gian
Nếu ai có gợi ý về một bo mạch FPGA giá rẻ có PCI-e hard IP thì rất mong được cho biết
[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...
Tuy nhiên giao tiếp tốc độ cao bên ngoài chỉ có một cổng USB 3.1 Gen 1
https://shop.lambdaconcept.com/home/50-screamer-pcie-squirre...
Litefury là kit Xilinx Artix FPGA dạng “NVMe SSD” (2280 Key M), dùng Xilinx XC7A100T và có giá 102 euro
Chỉ có vài ngõ vào/ra LVDS tốc độ cao bên ngoài
https://rhsresearch.com/collections/rhs-public/products/lite...
Vivado không phải là công cụ “tuyệt vời” theo tiêu chuẩn của kỹ sư phần mềm chuyên nghiệp, nhưng trong phát triển và triển khai FPGA thì chắc chắn thuộc hàng tốt nhất ngành
Lộ trình phát triển thiết bị PCIe của Xilinx cũng đã được chuẩn bị khá bài bản
Tôi chưa từng trực tiếp làm việc với driver thiết bị Linux, nhưng vài năm trước đã làm một số driver PCIe trên hệ điều hành khác, và các khái niệm trông rất quen thuộc
Mong có thêm nhiều nội dung kiểu này
Chỉ đưa vào vừa đủ code để cho thấy điểm cốt lõi, rồi xây dựng dần từng bước
Cả đời tôi chưa từng muốn tạo một thiết bị PCI mới, nhưng giờ thì hơi muốn thử, và tôi nghĩ đó chẳng phải là phép thử quyết định của một bài viết kỹ thuật hay sao
Tôi từng muốn tạo một môi trường phát triển và playtest cho dự án nhưng thậm chí còn không biết phải tìm bằng từ khóa nào; đây đúng là nội dung tôi cần
Hai phần còn lại cũng hay, với nhiều nội dung thực chiến như cách dùng mã driver boot services sau khi thoát, bus mastering, MSI-X, cùng nhiều chi tiết nhỏ nhưng hữu ích