FIRMから入ったFPGA初学者がVerilogに抱く疑問
ファームウェア(C / Python等)から入ったFPGA初学者が「なんでこう書くの?」と思ったポイントを整理しました。間違いがありましたら、コメントいただけると幸いです
■ verilogでの@ とは何か:「イベント待ち」の正体
@ は言語ごとに役割が異なりますよね
Pythonではデコレータ、Javaではアノテーション、、、、
Verilogではイベント制御で使われます
@ は「変化した瞬間だけ反応する仕組み」
@(posedge clk)
これは「クロックが 0 → 1 に変化した瞬間だけ反応する」という意味です。
- 「1である状態(レベル)」ではなく「変化(エッジ)」を捉えるものです
- @ はクロック専用ではなく、任意の信号の変化(エッジ)に使えます
瞬間 vs 継続
Verilog(FPGA) を理解するうえで最も重要な概念だと思います
同じalways構文でも、@がついたりつかなかったり...ややこしくないですか?
私は、エッジで変化するものは@がつき、レベルで変化するものはつけないという考えで腑に落ちました
| 種類 | 動作 | 概念 |
|---|---|---|
| @ が付く場合(always_ff) | 変化した瞬間だけ反応 | 瞬間(エッジ) |
| @がつかない(always_comb / assign) | 条件が成立している間ずっと有効 | 継続(レベル) |
「継続」の例
assign y = a & b;
a=1, b=1 の間はずっと y=1。a または b が変わると即座に更新される。
状態が続く限り、出力が決まり続けるのが継続の特徴です。
「瞬間」の例
always @(posedge clk) begin
q <= d;
end
clk が立ち上がった「瞬間」だけ実行されます。
その瞬間に d を取り込み、q を更新します。
CPUでいう「今このタイミングで保存する」という動作に近く、
FPGAではレジスタ(フリップフロップ)を作る基本形です。
wait との違い
@ と wait はどちらもイベント待ちですが、本質が異なります。
wait は「条件が成立するまで処理を停止する」という書き方で、
Verilogでは主にテストベンチ(シミュレーション)で使われます。
@(a) // a が変化した瞬間だけ反応
wait(a == 1); // a が 1 の状態である間ずっと成立
| 構文 | 動作 |
|---|---|
@(a) |
a が変化した時だけ反応 |
wait(a == 1) |
a がすでに 1 なら即通過。1 の間ずっと成立 |
@(a or b) に and がない理由
最初はorがあるのだから、andもあるんじゃないの?って思いませんでしたか?
@(a or b) // OK
@(a and b) // 存在しない(構文エラー)
理由:イベントは「瞬間」なので、2つの信号が完全に同時に変化することを保証できない。
- OR = どちらかが起きたらOK → 保証できる
- AND = 両方が同時 → 物理的に保証不可能
AND 相当の条件を書きたい場合は、中に条件として書きます:
always_ff @(posedge clk) begin
if (a && b) // 条件は中に書く(状態として扱う)
y <= 1;
end
■ always って関数みたいなものではないの?
よく出てくるalwaysってブロック単位のように見えますし、関数みたいなものでは?って思いませんでしたか?
always = 回路そのもの
always は「関数」でも「ループ」でもなく、常時動作する回路の定義です。
always + タイミング + 処理
always_ff @(posedge clk) begin // 順序回路 + clk 立ち上がりエッジ
q <= d; // 処理内容
end
| キーワード | 意味 |
|---|---|
| always_ff | 順序回路(FF)の宣言 |
| always_comb | 組み合わせ回路の宣言 |
| @(...) | いつ動くか(瞬間) |
always_comb に @ が不要な理由:イベント(瞬間)ではなく状態(継続)を扱うため、「いつ」を指定する必要がないからです。
■ = と <= の違い
=は代入演算子で、<= は比較演算子と思いませんでしたか?
「え、<= って比較じゃないの!?」って。
でもVerilogでは比較ではなく、「クロック後にまとめて代入する」という意味なんです。
最初はかなり脳がバグります。
= は「逐次処理(上から順に実行)」
<= は「同時更新(並列評価してまとめて更新)」
ブロッキング代入(=)
a = b;
c = a; // すでに更新された a を使う
→ c は 新しい a の値になる
ノンブロッキング代入(<=)
a <= b;
c <= a; // 元の a(更新前)を使う
→ 全右辺を先に評価してからまとめて更新
→ c は 古い a の値になる
なぜ2種類あるのか
ハードウェアの動作モデルに合わせるためです。
| 回路種別 | 使う代入 | 理由 |
|---|---|---|
| 組み合わせ回路 | = |
入力が変わったら即反映 |
| 順序回路(FF) | <= |
クロックで同時更新 |
迷ったらこのルールだけ守ればOK:
always_comb → =
always_ff → <=
■ @ と <= の関係
この2つはよく一緒に登場しますが、別概念です。
| 概念 | 意味 |
|---|---|
@ |
いつ更新するか(タイミング) |
<= |
どう更新するか(並列) |
always_ff @(posedge clk) begin
// ↑ いつ(瞬間) ↓ どう(同時更新)
q <= d;
end
■ reg / logic の正体
reg はレジスタではない
reg って書いてあるので、最初は
「なるほど、レジスタを作る型ね!」と思うんですよね。
でも実際には、reg と書いただけではレジスタにはなりません。
-
reg→ always 内で代入できる変数型(FF になるかは書き方次第) -
wire→ assign や外部出力の接続に使う型 -
logic→ SystemVerilog の統合型(reg / wire を一本化)
FF になるかどうかは always の書き方 で決まります:
// これは FF になる(posedge clk でトリガ)
always_ff @(posedge clk)
q <= d;
// これは組み合わせ回路になる(イベントトリガなし)
always_comb
y = a & b;
回路種類は「変数の型」ではなく「always の書き方」で決まる
■ 基本回路パターン
Verilogは文法だけ覚えても、なかなか回路として読めるようになりませんでした。
大事なのは、この書き方をすると、ハード的には何ができるのかを考えることだと思います。
実際に配置配線させて見てもいいと思います。
特にFPGAでは、よく使う回路パターンが何度も登場します。
最初は呪文に見えても、「あ、これMUXか!」と読めるようになると、一気に理解しやすくなります。
● MUX(セレクタ)
assign y = sel ? a : b;
複数の入力から1つ選ぶ回路。
● FF(フリップフロップ)
always_ff @(posedge clk)
q <= d;
値をクロック単位で保持する回路。
● カウンタ
always_ff @(posedge clk)
cnt <= cnt + 1;
分解すると:
- FF:現在値を保持
- 加算器:+1 を計算して次の値を作る
カウンタ = FF(状態保持) + 演算回路(次状態計算)
● enable 付きレジスタ
always_ff @(posedge clk) begin
if (en)
q <= d;
else
q <= q; // 保持
end
更新するか保持するかを MUX で選ぶ構造。
● MUX + FF
enable 付きレジスタと同じようなパターンですが。
always_ff @(posedge clk) begin
if (sel)
q <= a;
else
q <= b;
end
- if → MUX(どちらを選ぶか)
-
<=+ always_ff → FF(クロックで保持)
このパターンで「出力が必ず1つに決まる」が保証されます。
よくあるバグ集
Verilog初心者が一度は踏むあるあるバグを紹介します。
しかも厄介なのが、シミュレーションでは動いて見えること。。。
C言語感覚のまま書くと、FPGA特有の「同時に動く世界」にやられます。
私も最初、「なんで実機だけ変なんだ…?」を何回もやりました。
❌ 順序回路で = を使う
always @(posedge clk) begin
a = b;
c = a; // シミュ:新しい a / 実ハード:同時更新なので挙動が変わる
end
シミュレーションでは動いても実機で不一致が起きる。
✅ 正しい書き方
always @(posedge clk) begin
a <= b;
c <= a; // 古い a を使う → ハードの挙動と一致
end
❌ always_comb で <= を使う
always_comb begin
a <= b; // 同時更新という概念がない / ツールによってはエラー
end
✅ 正しい
always_comb begin
a = b;
end
■ 初学者あるある
PGAは「プログラム」ではなく「回路」を書く世界でなんですね。
既出の内容もありますが、再度初学者が一度は踏む Verilogあるある をまとめます。
① if は分岐だと思う
→ 実際は MUX。複数の候補から1つ選ぶ回路の記述。
② 上から順に処理されると思う
→ 実際は並列動作。ハードウェアは全ブロックが同時に動く。
③ reg はレジスタだと思う
→ ただの変数型。FF になるかは always の書き方次第。
④ 分岐網羅を忘れて、ラッチを生成させてしまう
always @(*) begin
if (en)
y = a; // else がない!
end
else がないと「en=0 のときどうするか」が未定義 → ラッチが自動生成されます。
system_verilogでは改善された構文が用意されています
修正:以下のように条件網羅させないといけません
always @(*) begin
if (en)
y = a;
else
y = 0; // 必ず全条件に代入する
end
⑤ とりあえず全部に reset を入れてしまう
最初の頃、私も「レジスタには全部 reset 必須」と思っていました。
昔のVHDL文化の影響もあり、とにかく全部初期化したくなるんですよね。
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
data <= 0;
else
data <= next_data;
end
これはfirm出身者とか直接違う話ですが。
最近のFPGAでは、必要ないレジスタにまで reset を入れるのは、あまり推奨されません。
理由は単純で、reset配線が増えるとファンアウトが巨大になり、配置配線が苦しくなる、
タイミング悪化の原因になるからです。
データパス系は、初回だけ無効化すれば十分なことも多く、「本当に reset が必要な制御系だけ入れる」という設計が増えています。
つまり、reset は“とりあえず全部入れるもの”ではないというのは、FPGA設計で重要な考え方です。
⑥ 複数ドライバ
always_ff @(posedge clk) q <= a;
always_ff @(posedge clk) q <= b; // 同じ q に 2 か所から代入!
「FF が 2 個できるだけだからいいのでは?」と思うかもしれませんが:
1つのレジスタに複数ドライバと解釈され、競合・合成エラーとなります
修正:
always_ff @(posedge clk) begin
if (sel)
q <= a;
else
q <= b; // MUX で 1 つに統合
end
組み合わせ回路も同様です:
always_comb y = a;
always_comb y = b; // NG:ドライバ競合
⑦ #delay が実機で動くと思う
→ 最初、firmの感覚で使っていましたが、効果ありませんでした。
シミュレーション専用。実機では無視されます。
⑧ シミュレーション = 実機だと思う
→ 別物です。代表的な不一致は以下の通りです:
-
=の使い方によるタイミング差 - 初期値の扱い(シミュは 0 スタートが多いが実機は不定)
- 意図しないラッチ・複数ドライバはシミュでは気づかず実機で壊れる
■ まとめ:設計のルール化
- 1信号 = 1ドライバ(複数 always から同じ信号に代入しない)
- 出力は必ず1つに決まる構造にする
- 状態は FF で持つ
- 組み合わせ回路は全条件に代入する(ラッチ防止)
■ 感想
最初、Verilogは「C言語っぽい見た目の別物」だと感じました。
文法自体はそこまで難しくないのに、考え方が根本から違います。
特に混乱したのは、「順番に実行される世界」ではなく、「全部同時に存在する回路を書く世界」だということでした。
if は分岐ではなくMUX、always は処理ではなく回路、<= は比較ではなく同時更新。
ソフトウェア感覚で読むと、かなり脳が混乱します。
ただ、ある時から「これはプログラムではなく、回路図を文章で書いているだけなんだ」と考えるようになって、一気に理解しやすくなった気がします。
この記事が、同じように firmware / software 側から FPGA に入った人の助けになれば嬉しいです。