1 điểm bởi GN⁺ 2024-07-29 | 1 bình luận | Chia sẻ qua WhatsApp

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:
    1. 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).
    2. CPU báo cho card biết đã sẵn sàng bắt đầu sao chép.
    3. 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 pciemuLinux Kernel Labs - Device Drivers.

1 bình luận

 
GN⁺ 2024-07-29
Ý 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ị

    • Đã bắt đầu với Tang Mega 138k nhưng do tài liệu không nhiều nên đang mất khá nhiều thời gian
    • Muốn được gợi ý các bo mạch FPGA giá rẻ khác có PCI-e hard IP
  • Rất thích mạch triển khai của loạt bài này

    • Cách giải thích trọng điểm với lượng mã vừa đủ và xây dựng dần dần là rất hay
    • Đây là một ví dụ về bài viết kỹ thuật tốt khiến người ta muốn tự tạo một thiết bị PCI mới
  • 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

    • Tôi chưa từng làm việc với trình điều khiển thiết bị Linux, nhưng đã có kinh nghiệm làm nhiều trình điều khiển PCIe trên các hệ điều hành khác
    • Các khái niệm tạo cảm giác rất quen thuộc
    • Mong sẽ có thêm nhiều nội dung kiểu này
  • Thật sự cảm ơn vì đã viết bài này

    • Rất bổ ích và thực tế
    • Thông tin kiểu này trong lĩnh vực này thực sự rất hiếm
    • Bài viết cung cấp thông tin cần thiết để xây dựng môi trường phát triển/playtest cho dự án
    • Hai phần còn lại cũng rất thực tế
      • Bao gồm nhiều chi tiết hữu ích như cách dùng trình điều khiển bootsvc, bus mastering, msi-x, v.v.