画面を消す DIR ―― 共有されたロックを追って
Neo6502 の Hercules モードでは、起動後の最初の DIR コマンドで画面が消えました。十二の修正、うち十一は測定に否定され、四つの計測器が誤較正。原因は SDK のロックが二つのコアで共有され、映像割り込みをわずか一ライン遅らせていたことでした。一ラインで十分だったのです。
Neo6502 は 65C02 と RP2040 を組み合わせた機体で、映像出力を含む残りのすべてを RP2040 が担います。DVI 出力には Luke Wren 氏の PicoDVI を用います。当方のファームウェア Trinity は「Hercules」モードを追加しました。60 Hz の 720 × 480 信号のなかに 720 × 350 のモノクロ画面を収めるネイティブモードで、画面 1 行が映像 1 行に対応します。320 × 240 モードは各行を 2 度描くため、1 行あたりの時間が 2 倍あります。
不具合は再現可能で、しかも不条理でした。MODE 1 は動く。ところが 最初の DIR で画面が消える。2 回目は何事もなく通る。障害中もキーボードは応答していました。画面が戻るのはモードを切り替えたときだけです。
1. 測定が否定した仮説、ひとつずつ
2 つのコマンドの違いは FatFs のキャッシュでした。コールドでは 1 回目が USB メモリーから 22 セクターを読み、ウォームではほとんど読みません。引き金はディスク負荷です。残る問いは、それがどのように表示を壊すのかでした。
はじめはメモリー不足を疑い、次にバッファ不足、次にバス調停、次にモニター側の同期外れを疑いました。どの説明も次の測定で否定され、その上で出した 2 つの修正は、撤回されるまで不具合を悪化させました。
- ライン・バッファを 2 本から 4 本へ:黒画面が両コアの完全停止に変わりました。近隣の pico-pacPlus は当方の予想と逆のことを記録しています。「deeper line pools widen the window」。
- メモリー・バスで映像コアを優先:ディスクに一度も触れないまま、Hercules モードに入った時点で画面が消えました。
- クロックを 270 から 250 MHz へ下げる。ikjordan/PicoDVI から借りた 50 Hz の 720 × 540 タイミングを用いました。効果なし。しかしこれが正しい手がかりでした。減速して何も変わらないなら、帯域の問題ではない。
MDA カードのテキスト・モードと同じ、幅がちょうど 720 ピクセルの VGA タイミング 720 × 400 / 70 Hz も試しました。モニターはついにモード名を表示しました。720 × 480(テレビ向けのタイミング)では一度もなかったことです。ただし画像は崩れており、この手がかりは保留にしました。
2. 計測器が測定を誤らせる ―― 四度
ファームウェアのデバッグ用シリアルは一度も動きませんでした。拡張コネクターに線が出ていなかったのです。計測はすべて SWD プローブで行いました。動作中の RP2040 のメモリーを読み出せます。ファームウェアのキーボード・キューに直接書き込んで機体にタイプし、カウンターを読み出す道具を書きました。画面が黒いままでも使えます。
その道具は 4 度こちらを欺き、そのたびに仮説 1 つを失いました。
- 第 2 のコアをプローブにつなぐと、エンコーダーが毎秒 15 フレームまで落ち、存在しない障害を作り出します。ひと続きの測定を丸ごと捨てました。
- 校正の誤ったオーバーラン・カウンターが、正常動作中に毎秒 96 000を数えていました。各 DMA チャネルが自分のレーンのブロックごとに 1 回起動することを見落としていたのです。
- レイテンシー・カウンターが垂直ブランキングを測っていました。1 462 µs、待機中も負荷中も同じ値です。行番号ではなく持続時間で絞り込む必要がありました。ファームウェアは起動時にこのカウンターを意図的に 2 行ずらすからです。
- プローブからの
resetは、Hercules モードが両コアを固める状態を基板に残します。この要因を管理せずに取った測定がいくつもありました。
採用した規則:測定はすべて電源の入れ直しから始め、ソフトウェア・リセットからは始めない。そして、同じ状態から出発していない 2 つの試行を比べない。
3. ブラックボックスが示したもの
読み出しはどれも障害が定着したあとのものでした。そこでブラックボックスを仕込みました。異常を検出した正にその瞬間、いかなる修復よりも先に、6 つの DMA チャネルと映像ステート・マシンの全状態をファームウェアが写し取ります。
結果は明快でした。PicoDVI は 3 本の TMDS レーンそれぞれに、データ・チャネルと、それを再設定する制御・チャネルを使います。障害中、2 つのデータ・チャネルが別のレーンの設定を抱えていました。要求信号もちがい、FIFO もちがう。2 つのチャネルが同じ FIFO に書き、3 つめには何も届かない。一方、メモリー上のブロック・リストは無傷でした。壊れていたのはデータではなく、その読み込みです。
画面の写真が、10 回の読み出しよりもよく図を完成させました。DIR のあと、文字が青と黄の 2 つの複製に二重化し、約 40 ピクセル離れていました。黄 = 赤 + 緑。つまり他の 2 チャネルは互いに揃ったままで、青だけがずれていたのです。青は同期レーンであり、他が 2 個の制御ブロックを持つところ、唯一 4 個を持ちます。1 段のずれでも移動量が同じにならず、3 本のレーンが同期を失います。
4. 理解する前に、無害にする
ここから、原因を直さない修正が生まれました。すべてのレーンに同じ数のブロックを与える——データ 2 レーンのブランキングを、同期レーンと同じように分割するのです。これならオーバーランは3 本を同じ位相でずらし、画像は 1 枚のまま保たれます。
基板上、DIR を 4 回続けて:オーバーランは依然として発生し(15 回)、しかし 3 レーンは揃って動き——位相差ゼロ——文字は鮮明でした。不具合は見えなくなりました。
役には立ちますが、満足はできません。本当の問いが残っていました。なぜ制御チャネルが 1 周先に進むのか。
5. 原因 ―― 二つのコアが知らずに共有していたロック
ようやく正しく絞り込んだレイテンシー測定が、控えめで決定的な数字を出しました。有効行 2 本の間隔は名目 32 µs。DIR 中の最悪値は 63 µs、つまりわずか 1 行の遅れで、それ以上は決してありません。長い停止も、数百マイクロ秒の危険区間もなく、1 行だけです。
そして 1 行で足ります。その遅れのあいだにデータ・チャネルはブロックを終え、制御チャネルへ連鎖します。制御チャネルは誰に頼まれるでもなく、自分でリストを進みます。1 周先に出て、レーンがずれ、あとはそのまま続くのです。
残る問いは、割り込みを止めていたのは何か。答えはロック番号にありました。PicoDVI は 4 つのキューを next_striped_spin_lock_num() で取得したスピンロックで守ります。この SDK 関数は、16 番から 23 番までを、呼び出すすべての側に順番で配ります——キュー、ミューテックス、アラーム・プール、そしてライブラリーが使うものすべて。基板で読み出すと、PicoDVI が持っていたのは 18 番と 19 番で、FatFs と USB スタックが反対のコアから取り得る番号でした。
決定的な細部は SDK の文書にあります。spin_lock_blocking はまず、待つ側のコアの割り込みを禁止するのです。ディスクの連続転送中にアプリ側のコアがロックを握っているあいだ、映像コアは目を閉じて待っていました。行割り込みは 1 行遅れて届きます。
SDK はこの妥協を率直に記しています。これらのロックは共有であり、範囲内で番号を配ることは 2 つの用途が同じ番号に当たる「確率を下げる」。確率であって、保証ではありません。
6. 修正は一行
// 修正前
dvi_init(&dvi0, next_striped_spin_lock_num(), next_striped_spin_lock_num());
// 修正後
static int slTmds = -1, slColour = -1;
if (slTmds < 0) { slTmds = spin_lock_claim_unused(true); slColour = spin_lock_claim_unused(true); }
dvi_init(&dvi0, slTmds, slColour);
他の誰も取れない専有ロック(共有範囲の外、24 番と 25 番)。基板で、22 セクターのコールド DIR、電源の入れ直しから始めた 1 回を含む 2 試行での測定値:
| 修正前 | 修正後 | |
|---|---|---|
| チャネルの先行 | 9〜15 | 0 |
| 行 2 本の間隔 | 63 µs | 33 µs(名目値) |
| DIR 中の遅延行 | +10〜+16 | +2 |
| 画面 | 黒、または二重の文字 | 正常 |
不具合は再現しなくなりました。これはもう迂回策ではありません。
7. なぜ他の誰も見ていないのか
他の PicoDVI 系プロジェクトがライブラリーをどう初期化しているかを見ました。原典、ikjordan のフォーク、xep80 のフォーク、pico-pacPlus。いずれも元の例の書き方、next_striped_spin_lock_num() を踏襲しており、それで十分に足りています。SDK の妥協が姿を見せるのは、次の 3 条件がそろう特別な場面だけです。
- アプリ側コアでの持続的な入出力——単発の読み出しではなく、セクターの連続転送;
- 行を 2 度描かないネイティブな映像モード、つまり取り返す余裕がない;
- 2 つのサブシステムに同じロックを割り当てる巡り合わせ。
SD カードからプログラムを読み込むエミュレーターは、はじめの 2 つを満たします。他の方が同じ場面に遭ったかは分かりません。見るにはプローブと DMA チャネル先行のカウンターが必要で、どこにも記述を見つけられませんでした。だからこの記事を書きました。この兆候に思い当たる方がいれば、当方が費やした 2 日を節約できます。
8. この追跡が残したもの
修正 12 件、うち 11 件は否定されました。校正し直すべき誤った測定が 4 件。そして事態を実際に動かしたのは 2 つ、どちらも読み出しからではありません。1 本のレーンだけがずれていることを示した画面の写真と、原因を与えたロック番号の読み出しです。
自分のカウンターで判定した修正は、判定されていない修正です。同じ夜に 2 度、画面が何も映さないまま、指標はすべて緑でした。
この不具合の兆候は覚えておく価値があります。RP2040 に一般的なものだからです。リアルタイム系のちょうど 1 単位——ここでは 1 行——の遅れで、クロック周波数に左右されない。帯域の問題ではありません。誰かがロックを握っているのです。
出典とリンク
- Wren6991/PicoDVI — 原典のライブラリー。RP2040 でソフトウェアにより DVI を生成します。
- ikjordan/PicoDVI — 50 Hz モードと幅 720 ピクセルを加えたフォーク。
- pico-pacPlus — 捨てられたバッファ、両コアの停止、赤い線 ―― 記録された近隣の不具合。
- pico-sdk — hardware_sync — 「striped」ロックとその妥協点。SDK 自身のコメントに書かれています。
- neo6502.com — Olimex のボードと、Paul Robson によるファームウェア。