はじめに
前編ではABC472BをShrike-Liteへ実装し、実機テストを実施しました。
ところが実機テストでこけました。
今回は実機動作不良の原因追及と対策を行います。
最優先のFAILはN=3
最初に実機FAILした3件のテストケースは、
| Case | N | bytes / bursts | Result | Expected |
|---|---|---|---|---|
| best_at_first_cut | 3 | 10 / 1 | 7573991 | 98 |
| burst_boundary_259 | 86 | 259 / 2 | 1737 | 1001 |
| deterministic_random_100 | 100 | 301 / 2 | 76912 | 11636 |
です。
burst_boundary_259だけならSPI通信の256byte境界を疑えます。
deterministic_random_100だけならN=100固有の問題かもしれません。
しかしbest_at_first_cut=[100,1,1]は、
N=3
10 byte
1 burst
です。
値も小さく17bitに十分収まります。
そこでこのcaseを最優先のデバッグ対象にしました。
正常なら、
total = 102
cut 0:
left = 100
right = 2
diff = 98
cut 1:
left = 101
right = 1
diff = 100
answer = 98
です。
まずSPI速度を疑う
37.5MHzへ内部clockを落としたことで、4MHz SPIとのタイミング余裕が減った可能性を最初に考えました。
まず最優先case [100,1,1] を使って、SPI通信クロックを変えた次の条件を比較します。
B1: input 4 MHz / answer 4 MHz
B2: input 2 MHz / answer 4 MHz
B3: input 4 MHz / answer 2 MHz
B4: input 2 MHz / answer 2 MHz
さらに、前回のtransaction状態が残っている可能性も疑い、
A1: 最初だけresetして20回連続
A2: 毎回resetして20回
と、reset条件も比較試験に追加します。
Thonnyの実機試験も自動化する
このDebugテストを人間がRunボタンを押しながら繰り返すのは面倒です。
そこでdebug runnerを作り、A1 / A2 / B1~B4 / 代表caseを1回の実行で走査するようにしました。
さらにCodexからThonnyを操作し、
script open
↓
F5
↓
実機試験
↓
Shell全文copy
↓
log保存
まで自動化しました。
結果として、1回のrunnerで112件の実機試験を取得できました。
2MHzでも直らない
結果は、
A1: 0 PASS / 20 FAIL
A2: 0 PASS / 20 FAIL
B1: 0 PASS / 10 FAIL
B2: 0 PASS / 10 FAIL
B3: 0 PASS / 10 FAIL
B4: 0 PASS / 10 FAIL
でした。
全体では、
PASS=20
FAIL=92
TOTAL=112
RESULT=FAIL
です。
入力SPIだけ2MHzでもダメ。回答だけ2MHzでもダメ。両方2MHzでもダメ。resetしてもダメ。
raw MISOの先頭dummyは112件すべて0x00で、代表PASSケースは同じ条件でも正常でした。
これで、単純なSPI速度margin、回答4byteの常時alignmentずれ、256byte境界だけの問題、単純な前transaction残留はかなり疑いが薄くなりました。
DistRAMの中身を直接見る
外側から条件を変えても直らないので、内部を観測します。
最初に知りたいのは、distributed RAMとFFの保持データが
RAM[0] = 100
RAM[1] = 1
RAM[2] = 1
total = 102
になっているかです。
debug用RTLでは、入力3個をshadow FFにも保存し、DistRAMをreadbackしてtotal_sumと一緒に返しました。
[100,1,1]をresetなし10回、毎回reset 10回の合計20回実行すると、
N=3
COUNT=3
BYTE_INDEX=0
SHADOW=100,1,1
RAM=100,1,1
TOTAL=102
を全20回で確認できました。
つまり、少なくともこの最優先caseでは、入力組立は正しい。DistRAM writeも正しい。格納内容も正しい。total_sumも正しい。
入力受信時点では設計通りの動作をしています。
CALC内部を観測したいが、デバッグ回路が入らない
次はさらに内部保持値、例えばCALCが実際に読んだ値、prefix、diff、answerなどをtelemetryで返そうとしました。
ところが今回のShrike-Lite実装は、CLB使用率が95%になるほど、リソースを目いっぱい使っています。
観測回路を増やしてPNRをかけると、
FATAL ERROR: The design cannot fit into the current geometry.
PnR failed
とエラーが出て、回路を追加できません。
デバッグ回路を載せるスペースがない。
これはこれで狭小FPGAらしい話です。
1clock待ってみる
distributed RAM内の主なデータが正しいことは確認ができています。
なので、次は、そのデータが処理される、最終集計回路を疑う事にします。
デバッグ回路の追加は厳しい状況なので、犯人を直接名指しするのは難しそうです。
そこでまずは状況証拠を探します。
最初は、データ読み出しから計算開始までの間に、1クロックのWAITを入れてみて、この区間で何かの問題があるかを見てみます。
CALC_READ
↓
CALC_WAIT
↓
CALC_EVALUATE
変更はこれだけです。
一発で112/112 PASS
WAIT付きdebug bitstreamを実機へ入れると、
PASS=112
FAIL=0
TOTAL=112
になりました。最優先caseも、
RAW_RX=00:00:00:62
answer=98
PASS
です。
あら、直っちゃった。
さらに、distributed RAMとtotal値を観測する回路を外し、最終productionとして別にSynth / PNRしたbitstreamでも112/112 PASSしました。
異なる配置でも同じく良好な結果になったため、単に「たまたま良い配置になった」だけではなさそうです。
どうやらこの区間で不良が発生していたようです。
原因はDistRAM readからCALCまでの物理境界
今回の実験から、問題がDistRAM readからCALCまでの境界にあることはかなり強く確認できました。
一方で、その物理的な内訳がRAM primitive側の実効read latencyなのか、RAM出力から後段CALCまでのsettle marginなのかは、今回の観測だけでは分離できていません。
つまり、不良が生じる境界と必要な修正はかなり明確になりましたが、その境界の内部で何が支配的だったかまでは断定する証拠は手に入っていません。
最終productionは3clock / candidate
最終版は、メモリ読み出しと集計計算開始の間に1クロックのWAITを挟みます。
CALC_READ
↓
CALC_WAIT
↓
CALC_EVALUATE
です。
1候補3clockなので、
3 × (N - 1)
最大N=100では、
3 × 99
= 297 clocks
です。内部clockは37.5MHzなので、
297 / 37.5MHz
= 7.92 us
です。
MicroPython側の待ち時間は元から10usなので変更不要ですね。
Icarusも524/524 PASS
最終productionでIcarusを再実行すると、
SUMMARY PASS cases=524 random=512 failures=0 max_calc_clocks=297
でした。全524ケースPASSです。
CALC clock数だけが198から297へ増えました。
最終Synth / PNR
distributed RAM推論も維持されています。
RAM64X1D ×34
Type=M 34/40
最終productionは、
| 項目 | 結果 |
|---|---|
| CLB LUT5 | 737 / 1120 |
| distributed-memory LUT5 | 272 |
| FF | 190 |
| CLB | 131 / 140 |
| BRAM | 0 / 8 |
| WNS | +2.648 ns |
| TNS | 0 |
| Achievable Frequency | 41.635 MHz |
でした。
面白いことに、FSMへ1状態増やしたのにCLBは134から131へ少し減りました。配置や論理最適化の結果なので、state数と面積は単純には対応しません。
Timing余裕も一時完成版より大きくなっています。
bitstreamから実機まで自動化する
今回の副産物として、開発ループもかなり自動化できました。
ForgeFPGA Workshop
↓
Synth / PNR / bitstream生成
↓
E:\abc472b.binへcopy + SHA-256確認
↓
ThonnyでF5
↓
shrike.flash()
↓
実機試験
↓
Shellログ回収
までCodexが人間操作なしで実行しています。
最終実機試験
最終productionで同じ112件runnerを実行しました。
WORK5_DONE PLAN=ALL PASS=112 FAIL=0 TOTAL=112 RESULT=PASS
resetなし20回、毎回reset20回、input / answer SPIの4/4、2/4、4/2、2/2MHz、代表FAIL / PASSケースをすべてPASSしていました。
実機ログも残す
今回の反省から、最終実機出力そのものも一次資料として保存しました。
hardware_logs/WORK7_SEQ01.log
hardware_logs/hardware_log_abc472b_final.log
GitHubに上げておきますので、読者がお手元のRun結果と比較するときにご利用ください。
最大ケースの処理時間
最後にShrike-Liteとしての処理時間を測りました。
入力は、
N = 100
L_i = 100000 × 100個
として、20回測定し、測定対象には、
- RP2040側の入力チェック
- N / L_iの301byte serialize
- SPI入力転送
- 10us CALC待ち
- 24bit answer組立
- 4byte回答転送
を含めた時間を測定しています。
bitstream flash、FPGA reset、SPI初期化、printは測定外です。
結果は、
BENCHMARK N=100 RUNS=20
MIN_US=4180
AVG_US=4223.1
MAX_US=4728
RESULT=PASS
でした。
平均約4.22msです。
ACがもらえそうですね。
FPGA内部のCALC処理時間は7.92usなので、全体の0.2%未満。今回も処理時間の大部分はRP2040側の処理とSPI通信です。
おまけ:普通にAtCoderへ提出するなら
最後に、いつものAtCoder提出用コードです。
module main;
integer N;
integer L [0:99];
integer i;
integer total;
integer left;
integer right;
integer diff;
integer best;
integer ret;
initial begin
ret = $fscanf(32'h8000_0000, "%d", N);
total = 0;
for (i = 0; i < N; i = i + 1) begin
ret = $fscanf(32'h8000_0000, "%d", L[i]);
total = total + L[i];
end
left = 0;
best = 32'h7fffffff;
for (i = 0; i < N - 1; i = i + 1) begin
left = left + L[i];
right = total - left;
if (left >= right)
diff = left - right;
else
diff = right - left;
if (diff < best)
best = diff;
end
$display("%0d", best);
end
endmodule
こちらなら、distributed RAMもSPIもTimingも気にする必要はありません😂
競技プログラミングとしてはこちらが正解ですね。
まとめ
今回の教訓は、SIMでPASSしても、最後は実機を見る です。
小さなFPGAを限界近くまで使うと、シミュレーションやTiming Reportだけでは終わらないことがあります。
ABC472Bは、そのあたりまで含めてかなりShrike-Liteらしい題材になりました。
前回:
Shrike-LiteでAtCoder問題を解く(41):ABC472B 前編 - DistRAMに100個の長さを覚える
次回:
Interlude: ABC473を見てみるの予定
コード全文と実装資料:
第42回コード全文と実装資料