Tìm hiểu PCI-e: Trình điều khiển và DMA
Tóm tắt phần trước
- Ở phần trước, đã triển khai một thiết bị PCI-e đơn giản và tìm hiểu cách đọc/ghi từng 32 bit bằng địa chỉ thủ công (
0xfe000000).
- Để lấy địa chỉ này theo cách lập trình, cần yêu cầu hệ thống con PCI cung cấp chi tiết ánh xạ bộ nhớ.
Tạo cấu trúc trình điều khiển
- Cần tạo
struct pci_driver, đồng thời cần có bảng thiết bị được hỗ trợ và hàm probe.
- Bảng thiết bị được hỗ trợ gồm một mảng các cặp ID thiết bị/nhà cung cấp.
static struct pci_device_id gpu_id_tbl[] = {
{ PCI_DEVICE(0x1234, 0x1337) },
{ 0, },
};
- Hàm
probe sẽ được gọi khi ID thiết bị/nhà cung cấp khớp nhau, và cần cập nhật trạng thái trình điều khiển để tham chiếu tới vùng nhớ của thiết bị.
typedef struct GpuState {
struct pci_dev *pdev;
u8 __iomem *hwmem;
} GpuState;
Triển khai hàm probe
- Kích hoạt thiết bị và lưu tham chiếu tới
pci_dev.
static int gpu_probe(struct pci_dev *pdev, const struct pci_device_id *id) {
int bars;
unsigned long mmio_start, mmio_len;
GpuState* gpu = kmalloc(sizeof(struct GpuState), GFP_KERNEL);
gpu->pdev = pdev;
pci_enable_device_mem(pdev);
bars = pci_select_bars(pdev, IORESOURCE_MEM);
pci_request_region(pdev, bars, "gpu-pci");
mmio_start = pci_resource_start(pdev, 0);
mmio_len = pci_resource_len(pdev, 0);
gpu->hwmem = ioremap(mmio_start, mmio_len);
return 0;
}
Phơi bày card ra không gian người dùng
- Giờ đây, khi trình điều khiển kernel đã ánh xạ không gian địa chỉ BAR0, có thể tạo một thiết bị ký tự để ứng dụng không gian người dùng tương tác với thiết bị PCIe thông qua các thao tác tệp.
- Cần triển khai các hàm
open, read, write.
static int gpu_open(struct inode *inode, struct file *file);
static ssize_t gpu_read(struct file *file, char __user *buf, size_t count, loff_t *offset);
static ssize_t gpu_write(struct file *file, const char __user *buf, size_t count, loff_t *offset);
Sử dụng DMA
- Thay vì để CPU sao chép từng DWORD dữ liệu một, có thể dùng DMA để card tự sao chép dữ liệu.
- Định nghĩa giao diện "gọi hàm" DMA:
- CPU cho card biết dữ liệu cần sao chép (địa chỉ nguồn, độ dài), địa chỉ đích và hướng luồng dữ liệu (đọc hoặc ghi).
- CPU báo cho card biết đã sẵn sàng bắt đầu sao chép.
- Card báo cho CPU biết việc truyền đã hoàn tất.
#define REG_DMA_DIR 0
#define REG_DMA_ADDR_SRC 1
#define REG_DMA_ADDR_DST 2
#define REG_DMA_LEN 3
#define CMD_ADDR_BASE 0xf00
#define CMD_DMA_START (CMD_ADDR_BASE + 0)
static void write_reg(GpuState* gpu, u32 val, u32 reg) {
iowrite32(val, gpu->hwmem + (reg * sizeof(u32)));
}
void execute_dma(GpuState* gpu, u8 dir, u32 src, u32 dst, u32 len) {
write_reg(gpu, dir, REG_DMA_DIR);
write_reg(gpu, src, REG_DMA_ADDR_SRC);
write_reg(gpu, dst, REG_DMA_ADDR_DST);
write_reg(gpu, len, REG_DMA_LEN);
write_reg(gpu, 1, CMD_DMA_START);
}
Thiết lập MSI-X
- Vì việc thực thi DMA là bất đồng bộ, tốt hơn nên chặn
write cho đến khi hoàn tất.
- Card PCI-e có thể gửi tín hiệu cho CPU thông qua ngắt báo hiệu bằng thông điệp (MSI).
- Để thiết lập MSI-X, cần cấp phát không gian để lưu vùng cấu hình cho từng ngắt (bảng MSI-X) và bitmap các ngắt đang chờ xử lý (PBA).
#define IRQ_COUNT 1
#define IRQ_DMA_DONE_NR 0
#define MSIX_ADDR_BASE 0x1000
#define PBA_ADDR_BASE 0x3000
static irqreturn_t irq_handler(int irq, void *data) {
pr_info("IRQ %d received\n", irq);
return IRQ_HANDLED;
}
static int setup_msi(GpuState* gpu) {
int msi_vecs;
int irq_num;
msi_vecs = pci_alloc_irq_vectors(gpu->pdev, IRQ_COUNT, IRQ_COUNT, PCI_IRQ_MSIX | PCI_IRQ_MSI);
irq_num = pci_irq_vector(gpu->pdev, IRQ_DMA_DONE_NR);
request_threaded_irq(irq_num, irq_handler, NULL, 0, "GPU-Dma0", gpu);
return 0;
}
Ghi thực sự có chặn
- Có thể dùng hàng đợi chờ với cơ chế ngắt để chặn
write.
wait_queue_head_t wq;
volatile int irq_fired = 0;
static irqreturn_t irq_handler(int irq, void *data) {
irq_fired = 1;
wake_up_interruptible(&wq);
return IRQ_HANDLED;
}
static ssize_t gpu_fb_write(struct file *file, const char __user *buf, size_t count, loff_t *offset) {
GpuState *gpu = (GpuState*) file->private_data;
dma_addr_t dma_addr;
u8* kbuf = kmalloc(count, GFP_KERNEL);
copy_from_user(kbuf, buf, count);
dma_addr = dma_map_single(&gpu->pdev->dev, kbuf, count, DMA_TO_DEVICE);
execute_dma(gpu, DIR_HOST_TO_GPU, dma_addr, *offset, count);
if (wait_event_interruptible(wq, irq_fired != 0)) {
pr_info("interrupted");
return -ERESTARTSYS;
}
kfree(kbuf);
return count;
}
Hiển thị lên màn hình
- Giờ đây đã có một 'framebuffer' cho phép chuyển dữ liệu từ không gian người dùng tới thiết bị PCI-e thông qua
write(2).
- Có thể nối bộ đệm của card vào đầu ra console của QEMU để khiến nó trông giống như một GPU đang hoạt động.
struct GpuState {
PCIDevice pdev;
MemoryRegion mem;
QemuConsole* con;
uint32_t registers[0x100000 / 32];
uint32_t framebuffer[0x200000];
};
static void pci_gpu_realize(PCIDevice *pdev, Error **errp) {
gpu->con = graphic_console_init(DEVICE(pdev), 0, &ghwops, gpu);
DisplaySurface *surface = qemu_console_surface(gpu->con);
for(int i = 0; i<640*480; i++) {
((uint32_t*)surface_data(surface))[i] = i;
}
}
static void vga_update_display(void *opaque) {
GpuState* gpu = opaque;
DisplaySurface *surface = qemu_console_surface(gpu->con);
for(int i = 0; i<640*480; i++) {
((uint32_t*)surface_data(surface))[i] = gpu->framebuffer[i % 0x200000 ];
}
dpy_gfx_update(gpu->con, 0, 0, 640, 480);
}
static const GraphicHwOps ghwops = {
.gfx_update = vga_update_display,
};
Tóm tắt của GN⁺
- Bài viết này đề cập đến trình điều khiển thiết bị PCI-e và DMA, đồng thời giải thích cách cho phép ứng dụng không gian người dùng tương tác với thiết bị PCIe thông qua trình điều khiển kernel.
- Trình bày cách dùng DMA để giảm tải cho CPU và tăng tốc độ truyền dữ liệu.
- Giải thích cách dùng MSI-X để gửi tín hiệu cho CPU khi truyền DMA hoàn tất.
- Trình bày cách mô phỏng và kiểm thử GPU trong môi trường ảo bằng QEMU.
- Các dự án có chức năng tương tự gồm
pciemu và Linux Kernel Labs - Device Drivers.
1 bình luận
Ý kiến trên Hacker News
Mục tiêu cuối cùng là dùng FPGA để tạo một bộ điều hợp hiển thị
Rất thích mạch triển khai của loạt bài này
Có vẻ như là một tài liệu nhập môn tuyệt vời về trình điều khiển thiết bị PCIe trên Linux
Thật sự cảm ơn vì đã viết bài này