1 คะแนน โดย GN⁺ 2024-07-29 | 1 ความคิดเห็น | แชร์ทาง WhatsApp

การเรียนรู้ PCI-e: ไดรเวอร์และ DMA

สรุปตอนก่อนหน้า
  • ในตอนก่อนหน้าได้อธิบายการสร้างอุปกรณ์ PCI-e แบบง่าย และวิธีอ่าน/เขียนทีละ 32 บิตโดยใช้แอดเดรส (0xfe000000) แบบกำหนดเอง
  • หากต้องการรับแอดเดรสนี้ด้วยวิธีทางโปรแกรม จำเป็นต้องขอรายละเอียดการแมปหน่วยความจำจากซับซิสเต็ม PCI
การสร้างโครงสร้างไดรเวอร์
  • ต้องสร้าง struct pci_driver และต้องมีตารางอุปกรณ์ที่รองรับพร้อมฟังก์ชัน probe
  • ตารางอุปกรณ์ที่รองรับประกอบด้วยอาร์เรย์ของคู่ค่า device/vendor ID
static struct pci_device_id gpu_id_tbl[] = {
  { PCI_DEVICE(0x1234, 0x1337) },
  { 0, },
};
  • ฟังก์ชัน probe จะถูกเรียกเมื่อ device/vendor ID ตรงกัน และต้องอัปเดตสถานะของไดรเวอร์ให้สามารถอ้างอิงพื้นที่หน่วยความจำของอุปกรณ์ได้
typedef struct GpuState {
  struct pci_dev *pdev;
  u8 __iomem *hwmem;
} GpuState;
การทำฟังก์ชัน probe
  • เปิดใช้งานอุปกรณ์และเก็บรีเฟอเรนซ์ของ 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;
}
เปิดให้อุปกรณ์เข้าถึงได้จาก userspace
  • ตอนนี้เคอร์เนลไดรเวอร์ได้แมปพื้นที่แอดเดรส BAR0 แล้ว จึงสามารถสร้าง character device เพื่อให้แอปพลิเคชันใน userspace โต้ตอบกับอุปกรณ์ PCIe ผ่าน file operations ได้
  • ต้องทำฟังก์ชัน 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);
การใช้ DMA
  • แทนที่ CPU จะคัดลอกข้อมูลแบบ DWORD ครั้งละหนึ่งรายการ สามารถใช้ DMA เพื่อให้การ์ดคัดลอกข้อมูลด้วยตัวเองได้
  • นิยามอินเทอร์เฟซการ "เรียกฟังก์ชัน" ของ DMA:
    1. CPU บอกการ์ดถึงข้อมูลที่จะคัดลอก (แอดเดรสต้นทาง, ความยาว), แอดเดรสปลายทาง และทิศทางการไหลของข้อมูล (อ่านหรือเขียน)
    2. CPU แจ้งการ์ดว่าพร้อมเริ่มการคัดลอกแล้ว
    3. การ์ดแจ้ง CPU ว่าการส่งข้อมูลเสร็จสิ้นแล้ว
#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);
}
การตั้งค่า MSI-X
  • เนื่องจากการทำงานของ DMA เป็นแบบอะซิงโครนัส จึงดีกว่าหากให้ write บล็อกจนกว่าจะเสร็จ
  • การ์ด PCI-e สามารถส่งสัญญาณไปยัง CPU ผ่าน Message Signaled Interrupts (MSI) ได้
  • ในการตั้งค่า MSI-X ต้องจัดสรรพื้นที่สำหรับเก็บ configuration space ของแต่ละอินเทอร์รัปต์ (ตาราง MSI-X) และพื้นที่สำหรับเก็บบิตแมปของอินเทอร์รัปต์ที่รอดำเนินการ (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;
}
การเขียนแบบบล็อกจริง
  • สามารถใช้คิวรอร่วมกับกลไกอินเทอร์รัปต์เพื่อทำให้ 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;
}
แสดงผลบนหน้าจอ
  • ตอนนี้มี "เฟรมบัฟเฟอร์" ที่สามารถส่งข้อมูลไปยังอุปกรณ์ PCI-e จาก userspace ผ่าน write(2) ได้แล้ว
  • สามารถเชื่อมบัฟเฟอร์ของการ์ดเข้ากับเอาต์พุตคอนโซลของ QEMU เพื่อให้ดูเหมือน GPU ที่ทำงานได้จริง
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,
};

สรุปโดย GN⁺

  • บทความนี้กล่าวถึงไดรเวอร์อุปกรณ์ PCI-e และ DMA พร้อมอธิบายวิธีทำให้แอปพลิเคชันใน userspace โต้ตอบกับอุปกรณ์ PCIe ผ่านเคอร์เนลไดรเวอร์ได้
  • อธิบายวิธีใช้ DMA เพื่อลดภาระของ CPU และเพิ่มความเร็วในการส่งข้อมูล
  • อธิบายวิธีใช้ MSI-X เพื่อส่งสัญญาณไปยัง CPU เมื่อการโอนถ่าย DMA เสร็จสิ้น
  • กล่าวถึงวิธีใช้ QEMU เพื่อจำลองและทดสอบ GPU ในสภาพแวดล้อมเสมือน
  • โปรเจ็กต์ที่มีความสามารถคล้ายกัน ได้แก่ pciemu และ Linux Kernel Labs - Device Drivers

1 ความคิดเห็น

 
GN⁺ 2024-07-29
ความคิดเห็นบน Hacker News
  • เป้าหมายสุดท้ายคือการสร้างดิสเพลย์อะแดปเตอร์โดยใช้ FPGA

    • เริ่มต้นด้วย Tang Mega 138k แต่ใช้เวลาพอสมควรเพราะมีเอกสารไม่มาก
    • อยากได้คำแนะนำเกี่ยวกับบอร์ด FPGA ราคาย่อมเยาอื่น ๆ ที่มี PCI-e hard IP
  • ชอบลำดับการเล่าเรื่องของบทความชุดนี้มาก

    • อธิบายประเด็นสำคัญด้วยโค้ดที่มีเพียงพอและค่อย ๆ สร้างขึ้นทีละขั้น ซึ่งดีมาก
    • เป็นตัวอย่างของงานเขียนเชิงเทคนิคที่ดีซึ่งทำให้อยากลองสร้างอุปกรณ์ PCI ใหม่
  • ดูเหมือนจะเป็นบทนำที่ยอดเยี่ยมเกี่ยวกับไดรเวอร์อุปกรณ์ PCIe บน Linux

    • แม้จะไม่เคยทำงานกับไดรเวอร์อุปกรณ์บน Linux แต่เคยมีประสบการณ์ทำไดรเวอร์ PCIe หลายตัวบนระบบปฏิบัติการอื่น
    • แนวคิดต่าง ๆ ให้ความรู้สึกคุ้นเคยมาก
    • อยากเห็นคอนเทนต์ประเภทนี้มากขึ้น
  • ขอบคุณมากจริง ๆ ที่เขียนบทความนี้

    • มีประโยชน์และใช้งานได้จริงมาก
    • ข้อมูลแบบนี้หาได้ยากมากในสายงานนี้
    • ให้ข้อมูลที่จำเป็นสำหรับการสร้างสภาพแวดล้อมพัฒนา/ทดสอบเล่นสำหรับโปรเจกต์
    • อีกสองตอนก็ใช้งานได้จริงมากเช่นกัน
      • มีรายละเอียดที่มีประโยชน์มากมาย เช่น วิธีใช้ไดรเวอร์ bootsvc, bus mastering, msi-x เป็นต้น