【Verilog HDL】同期回路は徹底的に同期せよ

  • 1
    いいね
  • 2
    コメント

この記事はkstm Advent Calendar 18日目です。
http://qiita.com/advent-calendar/2016/kstm

Verilog HDL概要

 Verilog HDLとは、巷で話題なFPGA(Field-Programmable Gate Array)の構成を定義できるHDLの一種です。HDLの中でも、C言語ライクな文法、ソフトウェアシミュレーション環境サポート、等々が売りなようです【要出典】。某教員曰く、「ソフトウェア畑のプログラマでもハードウェアを扱えるようになる夢の環境」とのお言葉を頂きましたが、個人的な所感としては、ハードが絡んだ時点で闇です。

今回の課題

 2ch入力アナログパルス信号に任意のトリガをかけ、同時に立ち上がった時のみにカウントする

環境

  • ハードウェア
    • FPGA開発ボード(The Cyclone V GX Starter Board)
    • DAC/ADC搭載ドーターカード (AD/DA Data Conversion Card)
  • 開発環境

    • Quartus Prime Design Suite version 16.0 (Lite edition)
  • 開発下地

    • C5G_DCC.qpf (上記のハードのデモンストレーションプロジェクト)

偽実装

 パルスの立ち上がり->立ち下がりを検知するサブモジュールと、クロック単位時間でマージンを取って同時入力判定が出来るように、下記のように半ば脳死実装しました。かなり長いですが、要点は、サブモジュールは元モジュールの同期クロックで同期されていることと、マージンはサブモジュールの1bit出力をきっかけとしてカウントしていく方針で実現していることくらいでしょうか。

sub_module.v

module is_pulse (
    in_sig,      //入力信号
    clk,         //同期クロック
    threshold,   //立ち上がり検出の閾値
    reset_n,     //非同期リセット用入力
    is_pulse,    //パルスを検出したとき、1同期クロックだけ立ち上がる
    );

input   [13:0]  in_sig;
input           clk;
input   [13:0]  threshold;
input           reset_n;
output          is_pulse;

wire    [13:0]  in_sig;
wire    [13:0]  threshold;
wire            clk;
wire            reset_n;

reg             is_pulse;

reg     [31:0]  width_cnt;
reg             flag;


always @(negedge reset_n or posedge clk)
begin
    if (!reset_n) begin
        is_pulse    <= 0;
        pulse_width <= 0;
        width_cnt   <= 0;
        flag        <= 0;
    end
    else if (!flag) begin
        if (in_sig >= threshold && in_sig < 14'd8192) begin
            width_cnt   <= 1'b1;
            flag        <= 1'b1;
        end
        else begin
            is_pulse    <= 0;
        end
    end
    else begin
        if (in_sig < threshold || in_sig >= 14'd8192) begin
            is_pulse    <= 1'b1;
            width_cnt   <= 0;
            flag        <= 0;
        end
        else begin
            width_cnt   <= width_cnt + 1'b1;
        end
    end
end

endmodule
top_module.v
/*かなり長いので重要な部分のみ抜粋*/
//--- analog to digital converter capture and sync
    //--- Channel A
always @(negedge reset_n or posedge ADA_DCO)
begin
    if (!reset_n) begin
        per_a2da_d  <= 14'd0;
    end
    else begin
        per_a2da_d  <= ADA_D;
    end
end

always @(negedge reset_n or posedge sys_clk)
begin
    if (!reset_n) begin
        a2da_data   <= 14'd0;
    end
    else begin
        a2da_data   <= per_a2da_d;
    end
end

    //--- Channel B
always @(negedge reset_n or posedge ADB_DCO)
begin
    if (!reset_n) begin
        per_a2db_d  <= 14'd0;
    end
    else begin
        per_a2db_d  <= ADB_D;
    end
end

always @(negedge reset_n or posedge sys_clk)
begin
    if (!reset_n) begin
        a2db_data   <= 14'd0;
    end
    else begin
        a2db_data   <= per_a2db_d;
    end
end


wire            trigA;
reg     [15:0]  cntA;

wire            trigB;
reg     [15:0]  cntB;

reg     [15:0]  marginA;
reg     [15:0]  marginB;
reg     [15:0]  mixcnt;
//サブモジュールを2ch分呼び出し
is_pulse ipA(
    .in_sig(a2da_data),
    .clk(CLK),
    .threshold(14'd12288),
    .reset_n(reset_n),
    .is_pulse(trigA),
    );

is_pulse ipB(
    .in_sig(a2db_data),
    .clk(CLK),
    .threshold(14'd12288),
    .reset_n(reset_n),
    .is_pulse(trigB),
    );

//パルス検出の回数モニター用カウンタ
always @(negedge reset_n or posedge trigA)
begin
    if (!reset_n) begin
        cntA            <= 0;
    end
    else begin
        cntA            <= cntA + 1'b1;
    end
end

always @(negedge reset_n or posedge trigB)
begin
    if (!reset_n) begin
        cntB            <= 0;
    end
    else begin
        cntB            <= cntB + 1'b1;
    end
end
//マージンタイム設定
parameter threthold = 16'd32;
//マージンタイム内で、両信号が立ち上がったらmixcntをカウントアップ
always @(negedge reset_n or posedge CLK)
begin
    if (!reset_n) begin
        mixcnt          <= 0;
        marginA         <= 0;
        marginB         <= 0;
    end
    else begin
        if (marginA > 16'd0 && marginA < threthold && marginB > 16'd0 && marginB < threthold) begin
            mixcnt          <= mixcnt + 1'b1;
            marginA         <= 16'd0;
            marginB         <= 16'd0;
        end
        else begin
            if (marginA >= threthold) begin
                marginA         <= 16'd0;
            end
            else if (marginA > 16'd0) begin
                marginA         <= marginA + 16'd1;
            end
            else begin
                marginA[0]      <= trigA;
            end

            if (marginB >= threthold) begin
                marginB         <= 16'd0;
            end
            else if (marginB > 16'd0) begin
                marginB         <= marginB + 16'd1;
            end
            else begin
                marginB[0]      <= trigB;
            end
        end
    end
end


//7セグLEDでモニター
SEG7_LUT U1(.oSEG(SEG_OUT1),.iDIG(cntA[3:0]));
SEG7_LUT U2(.oSEG(SEG_OUT2),.iDIG(cntB[3:0]));
SEG7_LUT U3(.oSEG(SEG_OUT3),.iDIG(4'h0));
SEG7_LUT U4(.oSEG(SEG_OUT4),.iDIG(mixcnt[3:0]));

7秒間オンリー問題

 しかし、このコードをコンパイルして実機に流し込むと問題が生じます。Achに周期1000msで40nsの幅を持つパルスを、Bchに周期500msで40nsの幅を持つパルスをそれぞれ入力したところ、きっかり7秒間だけ同時入力カウントアップが動作し、その後は全く動作しなくなりました。
 非同期リセットをかけると、また7秒だけ動作する、7とは2^3-1であるなど、コード記述者側に問題がありそうな振る舞いがありましたが、この問題の不可解極まる現象として、if文の中でしか用いていないparameter threthold = 16'd32;の値を、例えば128にしたときは31秒だけ動作するという、謎の線形性を持つ再現性が認められたことを挙げます。
 

解決編

原因(?)

 怪しいところを洗っても、ネットの海を漁っても、なかなか解決に至らず、当初要素を追加するたびに整数でバージョンを上げて管理していたものが、少数第2位まで到達したときはどうしてくれようかと考えていました。
 いよいよ、手詰まりかと思いきや、公式のサポートに行き着きました。拙い英語力を駆使して訳すと、

レジスタの変更するタイミングに用いる信号がおかしそう。
ロジックを変更するか、信号を適切に制御してください。

とのことでした。正しいか検証できないので、話半分に聞いて欲しいのですが、ファンアウト数とかに影響されるらしいです。すごい掻い摘んで言うとハード故の問題でした。

真実装前準備

 今更ではありますが、Verilog HDLにおける変数のような何か、wireとregに言及します。wireは分かりやすく配線のことで、これにassignされているものは変更が即時に反映されます。。regはレジスタのことで、値の保持ができることから、こちらの方が変数っぽいです。が、値の変更タイミングはalways文+@(イベント)で制御するところはハードらしいところで、プログラマに求められるシリアルな考え方をぶち壊してくれます。回路って、基本的にパラレルで永続ですからね。

真実装

 問題は、"レジスタの変更するタイミング"、つまり、"always文+@(イベント)"のイベントに難ありということです。ここで、上記のコードを見返すと、サブモジュールからの出力をwireで受け取り、これをalwaysのイベントに使ったり、マージンタイムのきっかけなどに多く使われています。サブモジュールをクロックで同期させていたことから完全にノーマークでしたが、即時変更のあり得るwireです。怖いです。例え1bitでも、何か遅延があったら、何かチャタリングのようなものを起こしていたら、エトセトラ、とても怖いです。そこで、サブモジュールの出力をクロックで更新されるレジスタで同期を取り、これで各種イベントなどを駆動させて事なきを得ました。

neo_top_module.v
//以上上記と同文
wire            w_trigA;
reg             trigA;
reg     [15:0]  cntA;

wire            w_trigB;
reg             trigB;
reg     [15:0]  cntB;

reg     [15:0]  marginA;
reg     [15:0]  marginB;
reg     [15:0]  mixcnt;

is_pulse ipA(
    .in_sig(a2da_data),
    .clk(CLK),
    .threshold(14'd12288),
    .reset_n(reset_n),
    .is_pulse(w_trigA),
    .pulse_width(w_pulwidA)
    );

is_pulse ipB(
    .in_sig(a2db_data),
    .clk(CLK),
    .threshold(14'd12288),
    .reset_n(reset_n),
    .is_pulse(w_trigB),
    .pulse_width(w_pulwidB)
    );


//唯一にして最大の変更点
always @(negedge reset_n or posedge CLK)
begin
    if (!reset_n) begin
        trigA               <= 0;
        trigB               <= 0;
    end
    else begin
        trigA               <= w_trigA;
        trigB               <= w_trigB;
    end
end

//以下上記と同文

まとめ

結局パラメータをいじって動作時間が決まってしまう直接の原因は分からず終いでしたが、何物にも代え難き教訓を得ました。
もうタイトルにありますが、これです。
同期回路は徹底的に同期せよ