PCI-e学習: ドライバとDMA
前回の項目の要約
- 前回の項目では、簡単なPCI-eデバイスを実装し、アドレス(
0xfe000000)を手動で使って32ビットずつ読み書きする方法を扱った。
- プログラムからこのアドレスを取得するには、PCIサブシステムにメモリマッピングの詳細情報を要求する必要がある。
ドライバ構造体の作成
struct pci_driver を作成する必要があり、対応デバイステーブルと probe 関数が必要となる。
- 対応デバイステーブルは、デバイス/ベンダーIDの組の配列で構成される。
static struct pci_device_id gpu_id_tbl[] = {
{ PCI_DEVICE(0x1234, 0x1337) },
{ 0, },
};
probe 関数はデバイス/ベンダー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;
}
ユーザー空間にカードを公開
- これでカーネルドライバ内でBAR0アドレス空間をマッピングしたので、ユーザー空間アプリケーションがファイル操作を通じてPCIeデバイスと相互作用できるように、キャラクタデバイスを作成できる。
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が一度に1つのDWORDずつデータをコピーする代わりに、DMAを使ってカード自身にデータをコピーさせることができる。
- DMA「関数呼び出し」インターフェースの定義:
- CPUがカードに、コピーするデータ(ソースアドレス、長さ)、宛先アドレス、データフローの方向(読み取りまたは書き込み)を伝える。
- CPUがカードに、コピーを開始する準備ができたことを伝える。
- カードが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カードはメッセージシグナル割り込み(MSI)を通じてCPUに信号を送ることができる。
- MSI-Xを設定するには、各割り込み向けのコンフィグレーション空間(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;
}
画面に表示
- これでユーザー空間から
write(2) を通じてデータをPCI-eデバイスへ渡せる「フレームバッファ」ができた。
- 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を扱い、カーネルドライバを通じてユーザー空間アプリケーションがPCIeデバイスと相互作用できるようにする方法を説明している。
- DMAを使ってCPUの負荷を減らし、データ転送速度を高める方法を扱っている。
- MSI-Xを使って、DMA転送完了時にCPUへ信号を送る方法を説明している。
- QEMUを使って仮想環境でGPUをシミュレートし、テストする方法を扱っている。
- 類似の機能を持つプロジェクトとして、
pciemu と Linux Kernel Labs - Device Drivers がある。
1件のコメント
Hacker Newsのコメント
最終目標はFPGAを使ってディスプレイアダプターを作ること
この記事群の流れがとても気に入った
Linux PCIeデバイスドライバーの素晴らしい入門書のように見える
この記事を書いてくれて本当に感謝している