PCI-e 学習:ドライバと DMA
(blog.davidv.dev)- ハードコードした BAR0 アドレスを直接 peek/poke していた段階から脱し、Linux PCI サブシステムで BAR メモリを見つけ、カーネルドライバがデバイスを初期化する
- ドライバは
struct pci_driverの ID テーブルとprobe関数から始まり、BAR0 を カーネル仮想アドレスにマッピングしたうえでユーザー空間からのアクセスを準備する /dev/gpu-ioキャラクタデバイスを通じてread(2)とwrite(2)をつなぎ、container_ofでファイル操作からドライバの状態を取り戻す- DWORD 単位のコピーでは 1.2MiB の転送に約 800ms かかったが、MMIO レジスタベースの DMA 呼び出しに切り替えると約 300µs レベルまで短縮された
- DMA 完了待ちは MSI-X 割り込みと wait queue で処理し、最終的に QEMU コンソールへ framebuffer の内容を表示する偽 GPU として動作する
BAR0 をカーネルドライバで見つけてマッピングする
- 以前の実装では、
lspciからコピーした BAR0 アドレス0xfe000000に対して 32 ビット単位で直接読み書きしていた - アドレスをハードコードしないために、Linux PCI サブシステムからデバイスのメモリマッピング情報を取得する
struct pci_driverには 2 つの主要フィールドが必要- 対応する device/vendor ID ペアのテーブル
- ID が一致したときに呼び出される
probe関数
- サンプルデバイスは
PCI_DEVICE(0x1234, 0x1337)にマッチする - ドライバ状態
GpuStateにはstruct pci_dev *pdevと BAR メモリ用のu8 __iomem * hwmemを保存する probe関数は次の順序でデバイスを準備するpci_enable_device_mem(pdev)でデバイスのメモリアクセスを有効化pci_select_bars(pdev, IORESOURCE_MEM)で利用可能なメモリ BAR のビットフィールドを取得pci_request_region(pdev, bars, "gpu-pci")で BAR アドレス空間の所有権を要求pci_resource_start(pdev, 0)とpci_resource_len(pdev, 0)で BAR0 の開始アドレスと長さを取得ioremap(mmio_start, mmio_len)で物理アドレスをカーネル仮想アドレスにマッピングする
pci_register_driverをmodule_initで呼び出すと、ブートログにmmio starts at 0xfe000000とカーネル仮想アドレスが出力される
ユーザー空間にキャラクタデバイスとして公開する
- BAR0 アドレス空間をカーネルドライバにマッピングしたあと、ユーザー空間プログラムが
read(2)とwrite(2)で PCIe デバイスとやり取りできるように キャラクタデバイスを作る - このドライバに必要なファイル操作は
open、read、writeの 3 つだけ GpuStateにstruct cdev cdevを追加し、setup_chardevで次の作業を行うalloc_chrdev_regionでデバイス番号を割り当てcdev_initとcdev_addでキャラクタデバイスを登録device_createで/dev/gpu-ioを作成
- init スクリプトに
/busybox mdev -sを追加して/dev/疑似ファイルシステムを埋める - 以後
/dev/gpu-ioはキャラクタデバイスとして見え、例では major 番号241、minor 番号0として表示される
container_of でファイル操作からドライバ状態を探す
write実装でstruct file*のprivate_dataはopenが埋める必要があるが、openは別のprivate_dataやuser_data引数を受け取らないstruct inodeにはキャラクタデバイスを指すstruct cdev *i_cdevポインタがあるGpuStateがstruct cdevを 埋め込んでいるため、container_of(inode->i_cdev, struct GpuState, cdev)でGpuStateポインタを取り戻せるgpu_openは取得したGpuStateをfile->private_dataに保存する- 以後
gpu_readとgpu_writeはfile->private_dataからGpuStateを取り出して使う - 初期の
read/writeは一度に DWORD 1 つずつ処理するgpu_readはioread32(gpu->hwmem + *offset)で読み取り、copy_to_userでユーザーバッファにコピーするgpu_writeはユーザーバッファから 4 バイトをコピーし、offset を 4 増やす
- 小さな転送では動作するが、CPU がパケットを 1 つずつ処理し続ける必要があるため、大きな転送では遅い
- 640×480、32bpp に相当する 1.2MiB の転送には約
800msかかる
MMIO レジスタで DMA 呼び出しを作る
- CPU が DWORD 単位のコピーを繰り返す代わりに、デバイスが直接データをコピーするよう DMA を使う
- 作業要求はメモリマップド I/O(memory-mapped IO)方式で送る
- 一部のメモリアドレスは DMA 呼び出しの引数として機能するレジスタのように使う
- 別のアドレスは関数呼び出しの実行を意味するコマンドのように使う
- DMA インターフェースには、CPU がデバイスに知らせるべき値がある
- コピーするデータの source アドレスと長さ
- destination アドレス
- データ方向:main memory 側へ、または main memory から
- コピー開始の準備ができたという信号
- デバイスは転送完了を CPU に知らせる必要がある
- サンプルレジスタは次のように定義される
REG_DMA_DIRREG_DMA_ADDR_SRCREG_DMA_ADDR_DSTREG_DMA_LEN
CMD_DMA_STARTは、レジスタ値を埋める動作と実際の DMA 開始を区別するコマンドアドレスとして使われる- カーネルドライバの
execute_dmaはiowrite32で方向、source、destination、長さを書き、最後にCMD_DMA_STARTに1を書く
QEMU デバイス側の DMA 処理
- QEMU アダプタの MMIO
gpu_writeは以前の実装を置き換え、DMA レジスタとコマンドを処理する - レジスタ領域への書き込みは
gpu->registers[reg]に値を保存する - コマンド領域で
REG_DMA_STARTが来ると DMA 方向を確認する DIR_HOST_TO_GPU方向ではpci_dma_readを呼び出す- host アドレスは
REG_DMA_ADDR_SRC - device アドレスは
gpu->framebuffer + REG_DMA_ADDR_DST - 長さは
REG_DMA_LEN
- host アドレスは
- それ以外の DMA 方向はサンプルコードで
Unimplemented DMA directionとして処理される - カーネルドライバの
gpu_fb_writeは次の手順でユーザーデータを DMA に渡すkmalloc(count, GFP_KERNEL)でカーネルバッファを割り当てcopy_from_userでユーザーデータをカーネルバッファにコピーdma_map_single(&gpu->pdev->dev, kbuf, count, DMA_TO_DEVICE)で DMA アドレスを生成execute_dma(gpu, DIR_HOST_TO_GPU, dma_addr, *offset, count)を呼び出しkfree(kbuf)でバッファを解放
- この方式はサンプルシステムで約 300µs と測定されるほど高速化する
DMA 完了を MSI-X 割り込みで知らせる
- DMA 実行は非同期なので、
writeが完了するまでブロックされるようにするとより便利 - PCI-e カードは Message Signalled Interrupts で CPU に信号を送れる
- MSI は専用の電気的接続を使う古典的な割り込みとは異なり、バス上の通常のメッセージパケットとして割り込みを届ける
- MSI-X 設定のため、QEMU デバイスには 2 つの領域を置く
- 各割り込み設定を保存する MSI-X table
- pending interrupt bitmap である PBA
- サンプル定数は次のとおり
IRQ_COUNTは1IRQ_DMA_DONE_NRは0MSIX_ADDR_BASEは0x1000PBA_ADDR_BASEは0x3000
- QEMU の
pci_gpu_realizeでmsix_initとmsix_vector_useを呼び出し、MSI-X を初期化する lspci -vvでは MSI-X が有効化され、vector table は BAR0 offset00001000、PBA は BAR0 offset00003000と表示されるpci_dma_readが終わったあと、msix_notify(&gpu->pdev, IRQ_DMA_DONE_NR)を呼び出して割り込みを送る
カーネル IRQ ハンドラと bus mastering
- カーネルドライバは
pci_alloc_irq_vectorsで MSI-X/MSI ベクタを割り当て、pci_irq_vectorで IRQ 番号を取得する request_threaded_irqでGPU-Dma0ハンドラを登録する- ブート後の
/proc/interruptsには、例のように IRQ24がPCI-MSIX-0000:00:02.0とGPU-Dma0として表示される - 最初は動作しないが、それはカードが CPU へ独立してメッセージを送る権限を持っていないため
- デバイスが CPU の介入なしにシステムメモリを直接操作できるようにする機能が bus mastering
- カーネルの
gpu_probeでpci_set_master(pdev)を呼び出すと、デバイスに bus master 権限が付与される - その後
writeを 2 回呼び出すと、カーネルログにIRQ 24 receivedが 2 回出力される
wait queue で実際の blocking write を実装する
- 割り込みベースの通知が準備できたら、Linux wait queue で
writeをブロッキング呼び出しに変えられる - グローバル状態として
wait_queue_head_t wqとvolatile int irq_fired = 0を置く - IRQ ハンドラは次の作業を行う
irq_fired = 1で完了状態を設定wake_up_interruptible(&wq)で待機中のスレッドを起こすIRQ_HANDLEDを返す
setup_msiにはinit_waitqueue_head(&wq)を追加するgpu_fb_writeは DMA 実行後にwait_event_interruptible(wq, irq_fired != 0)で割り込みを待つ- 待機中に割り込まれた場合は
-ERESTARTSYSを返す
QEMU コンソールに framebuffer を表示する
- ユーザー空間の
write(2)を受け取り、DMA で PCI-e デバイスへ渡す framebuffer ができたので、QEMU コンソール出力につないで動作する GPU のように見せる - QEMU の
GpuStateにQemuConsole* conを追加する pci_gpu_realizeでgraphic_console_initによりコンソールを作り、qemu_console_surfaceで display surface を取得する- 初期テストパターンは 640×480 範囲の surface データに値を埋めて表示する
vga_update_displayはgpu->framebufferの内容を QEMU display surface にコピーするdpy_gfx_update(gpu->con, 0, 0, 640, 480)で 640×480 領域を更新する- 以後 underlying device にパターンを書き込むと表示が変わる
- ソースコードは the Github repo にある
1件のコメント
Hacker Newsのコメント
始めるために Tang Mega 138k [0] を買ったものの、ドキュメントがあまり多くなく時間がかかっているとのこと。
PCI-e ハードIPを備えた安価な FPGA ボードのおすすめがあれば知りたいとのこと。
[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...
ただし、外部の高速インターフェースは USB 3.1 Gen 1 が 1 つだけ。
https://shop.lambdaconcept.com/home/50-screamer-pcie-squirre...
Litefury は「NVMe SSD」フォームファクタ(2280 Key M)の Xilinx Artix FPGA キットで、Xilinx XC7A100T を使っており、価格は 102 ユーロ。
外部の高速 LVDS 入出力は数本しかない。
https://rhsresearch.com/collections/rhs-public/products/lite...
Vivado はプロのソフトウェアエンジニア基準で「素晴らしい」ツールとは言えないが、FPGA の開発と実装においては間違いなく業界最高水準だ。
Xilinx のPCIe デバイス開発パスもかなりよく整備されている。
Linux のデバイスドライバを直接触ったことはないが、数年前に別の OS で複数の PCIe ドライバを扱ったことがあり、概念はとても見覚えがある。
こういう種類のコンテンツがもっと増えてほしい。
要点を示すのに十分な量だけコードを載せて、段階的に積み上げていくやり方が良い。
一生のうちで新しいPCI デバイスを作りたいと思ったことはなかったのに、今は少し作ってみたくなったし、これこそ良い技術文章のリトマス試験のようなものではないかと思う。
プロジェクト用の開発・プレイテスト環境を作りたかったのに、検索語すら分からなかったところ、まさに必要だった内容だった。
ほかの 2 編も良かったし、ブートサービスドライバのコードを終了後に使う方法、バスマスタリング、MSI-X のような実践的な内容や、細かいが役立つディテールがたくさんあった。