この記事は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出力をきっかけとしてカウントしていく方針で実現していることくらいでしょうか。
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
/*かなり長いので重要な部分のみ抜粋*/
//--- 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でも、何か遅延があったら、何かチャタリングのようなものを起こしていたら、エトセトラ、とても怖いです。そこで、サブモジュールの出力をクロックで更新されるレジスタで同期を取り、これで各種イベントなどを駆動させて事なきを得ました。
//以上上記と同文
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
//以下上記と同文
#まとめ
結局パラメータをいじって動作時間が決まってしまう直接の原因は分からず終いでしたが、何物にも代え難き教訓を得ました。
もうタイトルにありますが、これです。
同期回路は徹底的に同期せよ