Verilog
FPGA
DE0

ソフトウェアの人がVerilogでジュリア集合を表示するまでに難儀したこと

More than 1 year has passed since last update.

ソフトウェアの人が独学でVerilog、FPGAを使った時の悩みごとと、

ソフトウェアの人への独断指針のまとめです。

近年、FPGAに手をだすソフトウェアの人が自分も含め多くなっているが、Lチカの次のモノ作りをやる時の参考情報。あと自分の備忘録。


発端

https://www.youtube.com/watch?v=n_Mh2fqaqqs

小さいFPGAボードと小さなLCDで重めであろう図形がスイスイ動く事になんとも感動し、作って見たくなった。

これと同等のものを作るのを目標とした。


作ったものや環境など


  • DE0(CycloneⅢ)と

  • aitendo M032C1289TP(LCD)を使って

  • Verilogオンリーでジュリア集合を

  • リアルタイムに計算して描画する


結果

こきたなく、一般的に合っているか分からないVerilogの記述になったと思うが、それっぽいものはできた。

動画はここ (2017/4/2更新)

ソースはここ(github)


ソースは2017/4/1時点GitHubでは以下修正を行っています。

・2のべき乗で除算対応。

・ノンブロッキングにできるのはノンブロッキング代入に変更

・その他もろもろのコード整理

・ジュリア集合の計算moduleの整理。

・不要WAITの削除


同じようなことをする人向けの独自指針と感想は以下。


  • 大きな修正をめんどくさがらない

     ちまちま直して動作確認を繰り返すよりも、ザクッと直してトライ&エラーをする方が効率が良い。


  • きれいなソースを目指さない(最初は)

     いわゆる高級言語の綺麗さを求めると回り道をする事になる。


  • 回路的な学習にはあまりならなかった

     それなりにmodule、回路のイメージを持ってVerilogを記載していく事にはなるが、次の物作りの時に回路を書いてからHDLがかけるかというと出来ないと思う。


  • Verilogを書く上でのイメージ

     全般的にクロックで同時に動くイメージがあれば、ソフトウェアの人は関数的にmoduleを使っても良いかなぁと思う。

    ※2017/3/20追記 「関数的」というのはメソッド的の意味。



LCD表示まで

元々ハード寄りのスキルを高めたいと常々思っていた事もあり、まずはLCD購入から。DE0はもっていた。

最初、秋月の300円液晶を購入したが素人にはサッパリだった。

もう少し情報多めのものが良いということで、aitendoのM032C1289TPを購入。

M032C1289TPはSSD1289というコントローラが乗っていてSTM32で液晶制御が参考になった。

※もう一個中国語のサイトが参考になったがURL記録しておらず忘却。

当初verilogだけで表示しようと試みたが全然だったので、一旦NIOSを使ってCでやってみた。

色々とハマったが特筆はLCDの初期化だったと思う。

・SSD1289にどの様な初期化パラメータを与えるべきかヤバイくらいに分からない。

 SSD1289のデータシート見ても何となく分かるが分からない。

 WEBを漁ってそれっぽいのを色々を試していった。

結局1,2ヶ月かけて画面全体を1色で塗りつぶすことができたが、当初はハード不良なのかソフトがいけないのか見えないのが不安であった。

色々とトライ&エラーを繰り返すが真っ黒のままだとハード故障の可能性もあり、辛くなっていく。

最初はごく簡単な確実に動くであろうテストプログラムで確認する事をお勧めする。

ちょっと寄り道してマンデルブロ集合を書いてみた。

非常に遅く表示するのに30分かかった。

Verilogで書けば神速だとうとタカを括り、NIOSのままジュリア集合の表示を行ってみた。

ジュリア集合の描画についてはWEBに色々と落ちているので諸々のソースを参考にして表示にこぎつけた。

これも激遅だったがVerilogで書けば神速なんでしょと?楽観視。

(2017/4/2追記 結果、神速になったと思う)


WAITの方法

表示できることはわかったので、Cで作ったものをVerilogにして行く作業を開始。

最初のはまりポイントはそもそもLCD表示できないこと。

まず、LCD表示で必要な待ち処理。

Cでは何も考えずにSleepを使っていたが、Verilogでどうやるの?

次々とクロック起点の処理が流れてくるから(というイメージなので)、Sleepの概念は捨てる。(少なくとも自分は捨てることで道が開けた)

Sleepではなく、ステートを用意して指定クロック空回しする事となる。

そうすると処理の途中で状態を保持したまま空回しが必要となるので、Javaに慣れ親しんだ体では到底許容できないソースになっていく。

wait()で待ちができると書いているサイトもあるが、結局wait(条件)とする必要があり、誰かが「条件」を変えてあげる必要が出てくる為、当時はイマイチ使えなかった。(と記憶している)

また、論理合成できないWait、Sleep的なものもあったりして悩ましかった。

以下はWAITのHDL。正しい方法なのかは定かではない。


always @(posedge clk) begin
if(state == `CMD_PREPARE) begin
・・処理
state <= `WAIT;
next_state <= 次のステート;
wait_time <= `TIME_1US; // 待ちたいクロック数
end else if(次のステート) begin
・・処理
state <= `WAIT;
next_state <= 次のステート;
wait_time <= `TIME_1US;

// ここで事前に設定しておいたクロック分待つ
end else if(state == `WAIT) begin
wait_time <= wait_time -1;
if(wait_time <= 0) begin
state <= next_state;
end
end
end


for文について

Javaに慣れ親しんでいると容易にforでループしたくなるが、forのループ数は私の環境では5000が上限であり、また、動的なループ数だとCompileが通らない。

最終的にはほとんどforは使用せず、配列の初期化など固定回数で少ないループの処理でしか使用しなかった。

HDLでのループは、クロックごとの処理自体がループとなる(という見かたも強引だができる)。

その為、regやwireで状態を保持させてクロックごとに処理を進めていく記載をする事になる。

例えば、ジュリア集合のコア処理をCで書くと、一つのやり方としては以下の感じになる。


// 収束しない時の k の値を描画する色のネタにつかう
for (k = 1; k <= IMAX; k++) {
xN = x * x - y * y + cr; // 実部
yN = 2 * x * y + ci; // 虚部
if ((xN * xN + yN * yN) > E) {
break; // 収束しない
}
x = xN;
y = yN;
}
color = k;

このループをクロック毎に処理する。

以下は色々端折っているがVerilogの記載。

クロックごとの計算結果をこのmoduleのインスタンスを定義している親moduleで保持してもらう。

親module側でカウンタを持って、このmoduleの処理結果で収束判定をして色ネタを確定させた。

2017/3/20追記 以下はnatsutanさんからアドバイスを受ける前のソース。


JuliaCalc.v(旧 これでも一応動いてた)

module JuliaCalc(

input clk,
input enable,
input signed [31:0] x,
input signed [31:0] y,
input signed [31:0] cr,
input signed [31:0] ci,
output out_end,
output signed [31:0] out_xN,
output signed [31:0] out_yN,
output signed [31:0] dout);

always @(posedge clk) begin
if(enable == 1'b1) begin
end_flg = 1'b0;
w_x = x;
w_y = y;
w_cr = cr;
w_ci = ci;

x2 = (w_x * w_x);
y2 = (w_y * w_y);
xN = ((x2 - y2) / `JL_MUL) + w_cr; // x^2-y^2+Cr

a1 = 32'd2 * w_x;
a2 = a1 * w_y;
a3 = a2 / `JL_MUL;
yN = a3 + w_ci;

xyn2 = ((xN * xN) + (yN * yN));
end_flg = 1'b1;
end
end

assign dout = xyn2;
assign out_xN = xN;
assign out_yN = yN;
assign out_end = end_flg;
endmodule


2017/3/20追記 以下はnatsutanさんからアドバイスを受けてノンブロッキング代入にして、もろもろ整理したソース。


JuliaCalc.v(新)moduleにする必要なくなってきた。。

module JuliaCalc(

input clk,
input enable,
input signed [31:0] in_x,
input signed [31:0] in_y,
input signed [31:0] cr,
input signed [31:0] ci,
output out_end,
output signed [31:0] out_wx,
output signed [31:0] out_wy,
output signed [31:0] out_res);

reg calc_start = 1'b0;
wire signed [31:0] wx, wy;
wire end_flg;

always @(posedge clk) begin
if(enable == 1'b0) begin
calc_start <= 1'b0;
end_flg <= 1'b0;

end else if(enable == 1'b1 && calc_start == 1'b0) begin
calc_start <= 1'b1;

wx <= (((in_x**2) - (in_y**2)) / `JL_MUL) + cr; // x^2 - y^2 + cr
wy <= (((32'sd2 * in_x) * in_y) / `JL_MUL) + ci; // 2 * x * y + ci

end_flg <= 1'b1;
end
end

assign out_res = ((wx * wx) + (wy * wy));
assign out_wx = wx;
assign out_wy = wy;
assign out_end = end_flg;
endmodule


2017/4/1追記 さらに整理を進めて現状は以下に落ち着いている。


JuliaCalcMain.v (元のJuliaCalcの呼び出し側で計算することに変更)

module JuliaCalcMain(

input clk,
input enable,
input signed [31:0] in_x,
input signed [31:0] in_y,
input signed [31:0] cr,
input signed [31:0] ci,
output out_calc_end,
output [15:0] out_color);

wire [7:0] state;
wire [15:0] ite;

// 1クロックで確定しない場合は以下で値を保持
wire signed [31:0] work_x, work_y;

// ジュリア集合の計算
wire signed [31:0] result_x = (((work_x**2) - (work_y**2)) / `JL_MUL) + cr;
wire signed [31:0] result_y = (((32'sd2 * work_x) * work_y) / `JL_MUL) + ci;
wire signed [31:0] calc_result = (result_x**2) + (result_y**2);

always @(posedge clk) begin
if(enable == 1'b0) begin
state <= `CALC_JULIA;
ite <= 0;
work_x <= in_x;
work_y <= in_y;

end else begin

// 上限を超えたら収束扱い
if (calc_result > `E_LIMIT) begin
state <= `CALC_JULIA_END;
end

// 上限を超えてなければ再計算
else begin
work_x <= result_x;
work_y <= result_y;
ite <= ite + 16'd1;

// 計算回数が上限を超えた場合は収束しないと判断し、iteに固定値を代入
if(ite > `JULIA_ITE_MAX) begin
ite <= 16'b11111_101111_11001; // R_G_B

state <= `CALC_JULIA_END;
end
end
end
end

assign out_calc_end = (state == `CALC_JULIA_END ? 1'b1 : 1'b0);

// wire [4:0] red = ((ite / (`JULIA_ITE_MAX / 16'd32)) >> 0) & 5'b11111;
wire [4:0] red = 5'b11111;
wire [5:0] green = ((ite / (`JULIA_ITE_MAX / 16'd64)) >> 0) & 6'b111111;
wire [4:0] blue = ((ite / (`JULIA_ITE_MAX / 16'd32)) >> 1) & 5'b11111;

assign out_color = {red, green, blue};

endmodule



浮動小数点数について

ネットを検索すると実数はreal型だよという情報が得られるが、QuartusⅡv13ではコンパイルが通らない。(未サポート)

Verilogで浮動小数点数を実現するには中々複雑な記載する必要があり、固定小数点数もヤバメであった。

小数点数の演算結果を待ち合わせる根性はもっておらず、

実数なしでジュリア集合の計算ができるのか当初絶望したが、整数のみでいけた。

具体的には、1万倍8192倍した値で計算を突き進み、実数で計算するポイントで1万8192で割ることで、今回においては回避出来た。

上述のHDLのJL_MULで割っている箇所がそれにあたる。

2017/3/20追記 natsutanさんアドバイスにより8192倍に変更。


除算について

QuartusⅡでは、除算を使用すると1つの除算ごとに lpm_divide:DivN というメガファンクションが自動で作成されるが、これが892ロジックを消費する。

当初、除算を容易に使用していたため、開発後半でCycloneⅢのロジック数上限を大きく越えて苦労した。

除算の削減を行い、なんとか現実的なロジック数に留めることができたが、安易な除算利用は危険である。

※2017/3/20追記 natsutanさんのアドバイスにより2のべき乗で割る事でビットシフトになりlpm_divideが駆逐され、すんごくロジック消費少なくなりました。それにより論理合成も早くなって良いことずくめです。


シミュレータについて

今回のシミュレータの使い方は、Cで動作させた時のジュリア集合の計算結果と、

Verilogでの計算結果を比較する時にシミュレータ(ModelSim)を使用した。

NIOS(C)でprintf出力した値と、ModelSimを使用してファイル出力した値を比較。

この方法で結構デバッグが進んだが、修正後のHDLをFPGA上で動作させると動かないことがあった。

FPGA上で高速に処理が行われると、シミュレータとタイミングが異なるのか、出力画像がおかしくなる事象が発生した。

結果的にはFPGA上とシミュレータでの動作は異なると判断した(当然の感じもするが)。

なので、シミュレータは油断できない。

よくシミュレータで波形を確認みたいな記事があるが、そんなのやってられない。

今回は運良く波形確認はあまりやらずに済んだが、急がば回れは基本なので、デバッグ手順に波形確認も入れたほうがベストである。