↓ メインコンテンツへスキップ

PCIeで OS ごとハングする — 真因はバスのプロトコル違反

·3043 文字·7 分 FPGA デバッグ
目次

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本のバスを共有していました。あとで効いてくるので、頭の隅に置いておいてください。

Windows PC と PCIe でつながる Kintex UltraScale+ FPGA の構成図。フレームバッファの1フレーム完了割込みで DMA コントローラが起動し、AXI4 でデータを PCIe IP コアへ、PCIe で Windows PC へ画像を転送する。制御・ステータスは AXI4-Lite の別経路

つまり、転送のきっかけは「1フレーム完了」の割込みでした。だから、転送が止まったときに、その起点にある割込みを最初に疑ったわけです。

割込み経路を疑うのは、なぜ妥当だったか
#

症状から割込みへ、の筋道
#

転送が途中で止まって、そのまま OS ごと固まる。この症状から割込みを疑うのは、わりと自然な連想だと思います。転送はフレーム完了の割込みで駆動されているので、「その割込みが DMA コントローラに届いていない」「取りこぼしている」で説明できそうに見えるからです。実際、フレームバッファが割込みを上げてから DMA コントローラが動き出すまでには、設定を取りこぼしうる箇所がいくつもあります。

疑った箇所と、もっともらしく見えた理由
#

僕が最初に疑ったのは、次のあたりでした。

  • 割込みの enable が立っていない、あるいはマスクされている
  • エッジ/レベルの設定が、フレームバッファ側の信号と食い違っている
  • フレームバッファから DMA コントローラへの割込み結線・ルーティングの漏れ

どれも、もっともらしく見えました。割込み周りは設定項目が多くて、一つでも取りこぼすと転送が止まるからです。しかも、固まるのは負荷をかけたときだけ。散発的にしか再現しないので、「割込みの取りこぼしっぽいな」という第一印象を後押ししていました。(ここが後から効いてくる落とし穴でした)

対策を入れても、症状は再現した
#

打った対策と、それでも再現
#

enable とマスクを見直し、エッジ/レベルをフレームバッファ側の信号に合わせ、割込みの結線も確認し直しました。それでも、負荷をかけると同じように固まります。対策を入れて、それでも症状が再現する。これはつまり、疑っていた割込みの設定は原因ではなかった、ということです。

割込みの対策を入れて負荷をかけても再び OS がハングする様子と、仮説が棄却される流れの図

それでも、次の仮説へ移るのに時間がかかった
#

理屈ではそうなのですが、実際にはなかなか割込みから離れられませんでした。理由は再現性です。散発的にしか固まらないので、「対策が効いて、たまたま今回は再現しなかっただけかもしれない」という言い訳が、いつでもできてしまうんですよね。何回か通ると「効いたかも」に見えて、しばらくするとまた固まる。この繰り返しで時間を溶かしました。

逆に考えると、ここでの「対策を入れても再現した」は、失敗ではなく一番強い証拠でした。対策が正しく入っているのに症状が消えないなら、その仮説はもう捨てていい。散発的な再現に惑わされないよう、「N 回試して再現したか」を先に決めておけば、もっと早く割込みの外に目を向けられたと思います。

真因:ハンドシェイクを待たずにバス使用権を解放していた
#

AXI4-Lite の「完了」の決まり
#

目を向けた先が、さっきの制御・ステータス用の AXI4-Lite バスでした。AXI 系のバスは、すべてのやりとりが VALID と READY のハンドシェイクで進みます。送り手が VALID を上げ、受け手が READY を上げて、両方が同じクロックで真になった瞬間に、そのデータが渡ったことになります。

なお、AXI のチャネルにあるのはこの VALID と READY だけです。あとで出てくる「バス使用権」(arbiter の grant)は AXI の信号ではありません。複数のマスタを1本のバスにまとめるアービタが、どのマスタにバスを使わせるかを決める、内部の調停信号です。

読出しなら、応答チャネルの RVALID と RREADY がハンドシェイクして、はじめて1回の読出しが完了します。(書込みなら BVALID と BREADY です) 大事なのは「信号を出したか」ではなく「ハンドシェイクが成立したか」です。VALID を上げただけでは、まだ何も渡っていません。

RVALID と RREADY が同一クロックでハンドシェイクして読出しが完了し、その後にバス使用権が解放される正常時のタイミング図

バス使用権を早く手放すと、何が起きるか
#

問題は、この 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 の依存関係が明記されています。