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

  • 11
    いいね
  • 0
    コメント

ソフトウェアの人が独学でVerilog、FPGAを使った時の悩みごとと、
ソフトウェアの人への独断指針のまとめです。
近年、FPGAに手をだすソフトウェアの人が自分も含め多くなっているが、Lチカの次のモノ作りをやる時の参考情報。あと自分の備忘録。

発端

https://www.youtube.com/watch?v=n_Mh2fqaqqs
小さいFPGAボードと小さなLCDで重めであろう図形がスイスイ動く事になんとも感動し、作って見たくなった。
これと同等のものを作るのを目標とした。

作ったものや環境など

  • DE0(CycloneⅢ)と
  • aitendo M032C1289TP(LCD)を使って
  • Verilogオンリーでジュリア集合を
  • リアルタイムに計算して描画する

結果

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

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

  • 大きな修正をめんどくさがらない
     ちまちま直して動作確認を繰り返すよりも、ザクッと直してトライ&エラーをする方が効率が良い。

  • きれいなソースを目指さない(最初は)
     いわゆる高級言語の綺麗さを求めると回り道をする事になる。

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

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

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で書けば神速なんでしょと?楽観視。

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の処理結果で収束判定をして色ネタを確定させた。

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

浮動小数点数について

ネットを検索すると実数はreal型だよという情報が得られるが、QuartusⅡv13ではコンパイルが通らない。(未サポート)
Verilogで浮動小数点数を実現するには中々複雑な記載する必要があり、固定小数点数もヤバメであった。
小数点数の演算結果を待ち合わせる根性はもっておらず、
実数なしでジュリア集合の計算ができるのか当初絶望したが、整数のみでいけた。
具体的には、1万倍した値で計算を突き進み、実数で計算するポイントで1万で割ることで、今回においては回避出来た。
上述のHDLのJL_MULで割っている箇所がそれにあたる。

除算について

QuartusⅡでは、除算を使用すると1つの除算ごとに lpm_divide:DivN というメガファウンクションが自動で作成されるが、
これが892ロジックを消費する。
当初、除算を容易に使用していたため、開発後半でCycloneⅢのロジック数上限を大きく越えて苦労した。
除算の削減を行い、なんとか現実的なロジック数に留めることができたが、安易な除算利用は危険である。

シミュレータについて

今回のシミュレータの使い方は、Cで動作させた時のジュリア集合の計算結果と、
Verilogでの計算結果を比較する時にシミュレータ(ModelSim)を使用した。
NIOS(C)でprintf出力した値と、ModelSimを使用してファイル出力した値を比較。
この方法で結構デバッグが進んだが、修正後のHDLをFPGA上で動作させると動かないことがあった。
FPGA上で高速に処理が行われると、シミュレータとタイミングが異なるのか、出力画像がおかしくなる事象が発生した。

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

よくシミュレータで波形を確認みたいな記事があるが、そんなのやってられない。
今回は運良く波形確認はあまりやらずに済んだが、急がば回れは基本なので、デバッグ手順に波形確認も入れたほうがベストである。