PCIeで OS ごとハングする — 真因はバスのプロトコル違反
目次
Kintex UltraScale+ の FPGA ボードを PCI Express で Windows PC につなぎ、画像を転送するアプリケーションを動かしていたときの話です。フレームを送り始めると、しばらくして Windows ごとハングします。ブルースクリーンも出ず、イベントログにも何も残らず、電源ボタンの長押しでしか復帰できない、というやつでした。最初に疑ったのは割込みです。「Xilinx FPGA と PCIe で、割込みを見直しても OS が固まる」で困っている人に、遠回りした過程ごと残しておきます。結論から言うと、真因は割込みではなく、AXI バス上のプロトコル違反でした。
構成:PCIe で画像を1フレームずつ送る#
まず、どういう構成だったかを説明します。FPGA は Kintex UltraScale+ で、Windows PC とは PCI Express (PCIe) でつながっています。動かしていたのは画像転送のアプリケーションです。
データの流れはこうです。フレームバッファに1フレーム分を書き終えると、DMA コントローラに割込みが入ります。割込みを受けた DMA コントローラは、AXI4 のバス経由で PCIe IP コアへデータを送り始めます。あとは PCIe IP コアが、それを PCIe 経由で Windows PC へ転送する、という流れです。
もう1本、制御用の経路もあります。Windows 側のドライバは、DMA の設定やステータスの確認を、PCIe の BAR 越しにレジスタを読み書きして行います。この制御・ステータスの経路が AXI4-Lite で、複数のマスタが1本のバスを共有していました。あとで効いてくるので、頭の隅に置いておいてください。
つまり、転送のきっかけは「1フレーム完了」の割込みでした。だから、転送が止まったときに、その起点にある割込みを最初に疑ったわけです。
割込み経路を疑うのは、なぜ妥当だったか#
症状から割込みへ、の筋道#
転送が途中で止まって、そのまま OS ごと固まる。この症状から割込みを疑うのは、わりと自然な連想だと思います。転送はフレーム完了の割込みで駆動されているので、「その割込みが DMA コントローラに届いていない」「取りこぼしている」で説明できそうに見えるからです。実際、フレームバッファが割込みを上げてから DMA コントローラが動き出すまでには、設定を取りこぼしうる箇所がいくつもあります。
疑った箇所と、もっともらしく見えた理由#
僕が最初に疑ったのは、次のあたりでした。
- 割込みの enable が立っていない、あるいはマスクされている
- エッジ/レベルの設定が、フレームバッファ側の信号と食い違っている
- フレームバッファから DMA コントローラへの割込み結線・ルーティングの漏れ
どれも、もっともらしく見えました。割込み周りは設定項目が多くて、一つでも取りこぼすと転送が止まるからです。しかも、固まるのは負荷をかけたときだけ。散発的にしか再現しないので、「割込みの取りこぼしっぽいな」という第一印象を後押ししていました。(ここが後から効いてくる落とし穴でした)
対策を入れても、症状は再現した#
打った対策と、それでも再現#
enable とマスクを見直し、エッジ/レベルをフレームバッファ側の信号に合わせ、割込みの結線も確認し直しました。それでも、負荷をかけると同じように固まります。対策を入れて、それでも症状が再現する。これはつまり、疑っていた割込みの設定は原因ではなかった、ということです。
それでも、次の仮説へ移るのに時間がかかった#
理屈ではそうなのですが、実際にはなかなか割込みから離れられませんでした。理由は再現性です。散発的にしか固まらないので、「対策が効いて、たまたま今回は再現しなかっただけかもしれない」という言い訳が、いつでもできてしまうんですよね。何回か通ると「効いたかも」に見えて、しばらくするとまた固まる。この繰り返しで時間を溶かしました。
逆に考えると、ここでの「対策を入れても再現した」は、失敗ではなく一番強い証拠でした。対策が正しく入っているのに症状が消えないなら、その仮説はもう捨てていい。散発的な再現に惑わされないよう、「N 回試して再現したか」を先に決めておけば、もっと早く割込みの外に目を向けられたと思います。
真因:ハンドシェイクを待たずにバス使用権を解放していた#
AXI4-Lite の「完了」の決まり#
目を向けた先が、さっきの制御・ステータス用の AXI4-Lite バスでした。AXI 系のバスは、すべてのやりとりが VALID と READY のハンドシェイクで進みます。送り手が VALID を上げ、受け手が READY を上げて、両方が同じクロックで真になった瞬間に、そのデータが渡ったことになります。
なお、AXI のチャネルにあるのはこの VALID と READY だけです。あとで出てくる「バス使用権」(arbiter の grant)は AXI の信号ではありません。複数のマスタを1本のバスにまとめるアービタが、どのマスタにバスを使わせるかを決める、内部の調停信号です。
読出しなら、応答チャネルの RVALID と RREADY がハンドシェイクして、はじめて1回の読出しが完了します。(書込みなら BVALID と BREADY です) 大事なのは「信号を出したか」ではなく「ハンドシェイクが成立したか」です。VALID を上げただけでは、まだ何も渡っていません。
バス使用権を早く手放すと、何が起きるか#
問題は、この AXI4-Lite を複数のマスタで共有するときに、順番を割り振るアービタにありました。このアービタが、応答チャネルの READY ハンドシェイクを待たずに、バス使用権を次のマスタへ手放していたんです。
ハンドシェイクが成立する前にバス使用権が外れると、進行中のトランザクションが宙に浮きます。ここで効いてくるのが、ホストからのレジスタ読み(MMIO リード)です。MMIO リードは、応答が返るまで CPU が待つ「ノンポステッド」なアクセスです。だから AXI4-Lite 側で読出しのハンドシェイクが成立しないと、その読みは永遠に完了しません。PCIe の完了パケットも返らず、CPU はレジスタ1つの読みから戻ってこなくなる。これが「OS ごとハング」— ブルースクリーンもログも残さず、ただ固まる — の正体でした。割込みは最初から関係なかったわけです。
まとめ#
導入で挙げた「割込みを見直しても固まる」に戻ると、あれは割込みの設定が全部正しかったからこそ固まっていた、ということになります。見直すレイヤーが、そもそも一つ下 — 割込みではなく、ホストがステータスを読む AXI4-Lite のほう — だった、という話でした。
今回のケースの整理#
| 観点 | 今回のケース |
|---|---|
| 症状 | 転送中に OS ごとハング。ログも BSOD も残らない |
| 疑いやすい層 | 割込み(フレーム完了 → DMA の enable・マスク・エッジ/レベル・結線) |
| 真因の層 | バス(制御・ステータス用 AXI4-Lite のハンドシェイク) |
| 誤誘導した要因 | 散発的な再現。「効いたかも」に見えてしまう |
| 決定的な証拠 | 対策を入れても再現した |
持ち帰り#
- 「対策を入れて再現するか」を、仮説を捨てる判断そのものに使う。再現したら潔く捨てる
- 散発的な不具合ほど、先に「N 回試して再現したら本物」と回数を決めておく
- 割込みが届かないように見えるときは、割込み層の前に、その下のトランザクションが閉じているかを見る
- ハンドシェイク信号は「出したか」ではなく「VALID と READY が同じクロックで真になったか」で確認する
これはバスの話ですが、根っこは調光制御でも同じでした。たとえば I2C のクロックストレッチや DALI の応答待ちも、「相手の READY を待たずに次へ進むと壊れる」という同じ構造です。僕は照明と FPGA を両方やっていますが、この「ハンドシェイクを待てているか」という視点は、分野をまたいで効くなと思います。
参考にしたのは ARM の AMBA AXI Protocol Specification (IHI 0022) のハンドシェイク規定です。VALID/READY の依存関係が明記されています。