演算回路を書く
概要: シフトや乗算、大小比較など演算回路の記述を学び、論理合成して回路規模を確認する。
目標:
- 以下の回路の記述ができる
- 組み合わせ回路によるシフト回路
- 乗算回路
- 大小比較回路/一致比較回路
- 組み合わせ回路で作るROM
- 生成される回路の規模と、演算子や演算ビット幅との関連を説明できる
修了判定: 8ビットのシフト回路、一致比較回路、乗算回路を論理合成して回路規模の大小関係を確認する
まず実験:同じ加算を3つの書き方で突き合わせる
「+ と書くだけ」の加算が、回路としてどう作られているのかを先に見ておきます。ここでは4ビットの加算を、1ビット全加算器を4段つないだリプルキャリー、桁上げを式で展開したキャリールックアヘッド、そして + 演算子の3通りで書き、入力の全組合せで結果を突き合わせます。
3方式の加算器と自動判定テストベンチをダウンロード(ZIP)
回路の仕様
- 入力は
a[3:0]、b[3:0]、桁上げ入力cinです。 - 出力は方式ごとに和4ビットと桁上げ出力1ビットを持ちます。
- リプルキャリーは
sum = a ^ b ^ cin、cout = (a & b) | (a & cin) | (b & cin)の全加算器を4段つなぎます。 - キャリールックアヘッドは
g = a & b(generate)とp = a ^ b(propagate)から各桁の桁上げを展開します。 +演算子の結果を比較の基準にします。
3方式を並べた回路 adder4_styles.v を見る
`timescale 1ns/1ps
`default_nettype none
// One bit of addition, written out as gates.
module full_adder (
input wire a,
input wire b,
input wire cin,
output wire sum,
output wire cout
);
assign sum = a ^ b ^ cin;
assign cout = (a & b) | (a & cin) | (b & cin);
endmodule
// The same 4-bit addition written three ways:
// rc_* : ripple carry, four full adders chained
// cla_* : carry lookahead, carries expanded from generate/propagate
// ref_* : the + operator, used as the reference
module adder4_styles (
input wire [3:0] a,
input wire [3:0] b,
input wire cin,
output wire [3:0] rc_sum,
output wire rc_cout,
output wire [3:0] cla_sum,
output wire cla_cout,
output wire [3:0] ref_sum,
output wire ref_cout
);
// ripple carry
wire c1, c2, c3;
full_adder fa0(.a(a[0]), .b(b[0]), .cin(cin), .sum(rc_sum[0]), .cout(c1));
full_adder fa1(.a(a[1]), .b(b[1]), .cin(c1), .sum(rc_sum[1]), .cout(c2));
full_adder fa2(.a(a[2]), .b(b[2]), .cin(c2), .sum(rc_sum[2]), .cout(c3));
full_adder fa3(.a(a[3]), .b(b[3]), .cin(c3), .sum(rc_sum[3]), .cout(rc_cout));
// carry lookahead
wire [3:0] g = a & b; // generate
wire [3:0] p = a ^ b; // propagate
wire k1 = g[0] | (p[0] & cin);
wire k2 = g[1] | (p[1] & g[0]) | (p[1] & p[0] & cin);
wire k3 = g[2] | (p[2] & g[1]) | (p[2] & p[1] & g[0]) | (p[2] & p[1] & p[0] & cin);
assign cla_cout = g[3] | (p[3] & g[2]) | (p[3] & p[2] & g[1])
| (p[3] & p[2] & p[1] & g[0]) | (p[3] & p[2] & p[1] & p[0] & cin);
assign cla_sum = p ^ {k3, k2, k1, cin};
// reference
assign {ref_cout, ref_sum} = a + b + cin;
endmodule
`default_nettype wire
全入力を突き合わせるテストベンチ tb_adder4_styles.v を見る
`timescale 1ns/1ps
`default_nettype none
// Drives all 4-bit a, b and cin combinations: 16 x 16 x 2 = 512 patterns.
// Each pattern compares two hand-written adders with the + operator.
module tb_adder4_styles;
reg [3:0] a, b;
reg cin;
wire [3:0] rc_sum, cla_sum, ref_sum;
wire rc_cout, cla_cout, ref_cout;
integer checks = 0;
integer i;
adder4_styles dut(.a(a), .b(b), .cin(cin),
.rc_sum(rc_sum), .rc_cout(rc_cout),
.cla_sum(cla_sum), .cla_cout(cla_cout),
.ref_sum(ref_sum), .ref_cout(ref_cout));
task check_ripple;
begin
checks = checks + 1;
if ({rc_cout, rc_sum} !== {ref_cout, ref_sum})
$fatal(1, "FAIL: ripple a=%b b=%b cin=%b got=%b expected=%b",
a, b, cin, {rc_cout, rc_sum}, {ref_cout, ref_sum});
end
endtask
task check_lookahead;
begin
checks = checks + 1;
if ({cla_cout, cla_sum} !== {ref_cout, ref_sum})
$fatal(1, "FAIL: lookahead a=%b b=%b cin=%b got=%b expected=%b",
a, b, cin, {cla_cout, cla_sum}, {ref_cout, ref_sum});
end
endtask
initial begin
$dumpfile("adder4_styles.vcd");
$dumpvars(0, tb_adder4_styles);
$display(" a b cin | ripple lookahead +");
for (i = 0; i < 512; i = i + 1) begin
{a, b, cin} = i[8:0];
#1;
check_ripple;
check_lookahead;
if ((a == 4'd15 && b == 4'd1) || (a == 4'd15 && b == 4'd15) || (a == 4'd8 && b == 4'd8))
$display("%4d %4d %b | %b %b %b",
a, b, cin, {rc_cout, rc_sum}, {cla_cout, cla_sum}, {ref_cout, ref_sum});
#1;
end
$display("PASS: adder styles (%0d checks)", checks);
$finish;
end
initial begin
#20000;
$fatal(1, "FAIL: testbench timeout");
end
endmodule
`default_nettype wire
実行方法
iverilog -g2012 -Wall -s tb_adder4_styles -o sim.vvp adder4_styles.v tb_adder4_styles.v
vvp sim.vvp
a・bの16通りずつと cin の2通り、合わせて512パターンを与え、2方式をそれぞれ + と比べます。Windows / Icarus Verilog 13.0で2026年10月26日に実行した結果の抜粋です。
a b cin | ripple lookahead +
8 8 0 | 10000 10000 10000
15 1 0 | 10000 10000 10000
15 15 1 | 11111 11111 11111
PASS: adder styles (1024 checks)
15+1も15+15+1も、3つの書き方で同じ値になりました。桁上げが上の桁まで伝わる場面でも一致しています。
ここで確かめたのは論理機能が同じことだけです。どの方式が速いか、資源をどれだけ使うかは測っていません。それは対象デバイスと制約を決めて論理合成し、タイミング解析とレポートで比べる話です。機能のシミュレーションがPASSでも、動作周波数の保証にはならないんだ。
わざと間違えて、テストの働きも確かめる
| 壊した場所 | 結果 |
|---|---|
全加算器の cout から (b & cin) を落とす | ripple a=0000 b=0001 cin=1 got=00000 expected=00010 |
和を ^ から | に変える | ripple a=0000 b=0001 cin=1 got=00011 expected=00010 |
先読みの k2 から (p1 & p0 & cin) を落とす | lookahead a=0000 b=0011 cin=1 got=00000 expected=00100 |
どれも、桁上げが絡む入力で初めて食い違います。そこで、桁上げの起きないパターンだけで試すとどうなるかも確かめました。b=0、cin=0 のまま a を0〜15まで動かす16通りのテストベンチでは、桁上げの項を落とした回路でも合格します。
PASS: partial check (16 of 512 patterns, no carry)
※この桁上げの起きないテストベンチは、サイト側の検証スクリプトで実行したもので、配布ZIPには入っていません。ZIPに入っているのは全512パターンを回すほうです。
加算器のバグは、たいてい桁上げの経路に出ます。試すパターンに桁上げが含まれていなければ、テストは通ってしまうんだ。L1の法則の実習と同じで、入力の選び方が検出できる範囲を決めるよ。
確認問題
4ビットの加算で a=15、b=1、cin=0 を与えると、和は4ビットに収まりません。このとき何が起きるでしょうか。
答え
和は0になり、桁上げ出力が1になります。上の実行結果で 10000 と表示されているのがその状態で、先頭の1が桁上げ出力です。+ の結果を4ビットの変数だけで受けると、この桁上げは捨てられます。桁あふれを使う回路では、受け側を5ビットにするか、桁上げ出力を別の信号として扱ってね。ビット幅の扱いは文法実験4でも確かめられます。
この後の節では、シフト・乗算・比較・ROMの記述と、論理合成したときの規模の違いを見ていきます。
1. 組み合わせ回路によるシフト回路
シフト回路や乗算回路みたいに「計算する回路」は、1行で書けちゃうから簡単に見えるよね?🐾
でも、実は裏でとんでもなく巨大な回路ができているかもしれないから要注意だよ!
シフト回路は入力信号dinをsftで指定したビット数だけ左シフトしてdoutに出力する回路です。左シフトして空になった下位ビットには0が入ります。
シフト動作の例
| 信号 | 値(2進) | 値(16進) |
|---|---|---|
| din | 1 0 1 1 0 1 0 1 | 8'hb5 |
| sft | 3(3ビット左シフト) | |
| dout | 1 0 1 0 1 0 0 0(下位3ビットに0が挿入) | 8'ha8 |
シフト回路(8ビット入出力・3ビットシフト量)
module shift( din, sft, dout );
input [7:0] din;
input [2:0] sft;
output [7:0] dout;
assign dout = din << sft;
endmodule
- シフト動作は1行のassign文で記述できる(左シフト演算子
<<を使用) - 入出力ビット数が多いと論理合成後の回路規模が大きくなる
- 演算回路全般に言えることだが、論理合成を実施して回路規模が適切かどうか確認する習慣をつけること
2. 乗算回路
2つの入力a、bを掛け合わせた結果をqに出力します。4ビット入力 × 4ビット入力の場合、出力は最大8ビット幅が必要です。
module multi( a, b, q );
input [3:0] a, b;
output [7:0] q;
assign q = a * b;
endmodule
- 乗算演算子(
*)で入力aとbを掛け合わせ、結果を出力qに代入 - 入出力ビット数が多い場合、論理合成後の回路規模が大きくなる
- ASICメーカーやFPGAメーカーがあらかじめ乗算回路ブロックを用意している場合がある
- 論理合成で生成された回路規模と比較して、より小さくなる方を使用するのがよい
3. 大小比較回路と一致比較回路
2つの入力を比較し、その結果を1ビットの値として出力します。どちらもassign文1行で記述できます。
大小比較回路(a > b)
module comp_GT( a, b, a_gt_b );
input [7:0] a, b;
output a_gt_b;
assign a_gt_b = (a > b) ? 1'b1 : 1'b0;
endmodule
一致比較回路(a == b)
module comp_EQ( a, b, a_eq_b );
input [7:0] a, b;
output a_eq_b;
assign a_eq_b = (a == b);
endmodule
| 回路 | 演算子 | 出力が1になる条件 | 出力信号名 |
|---|---|---|---|
| 大小比較(GT) | >(関係演算子) | a > b のとき | a_gt_b |
| 一致比較(EQ) | ==(等号演算子) | a == b のとき | a_eq_b |
? :)を使う方法と、等号演算結果を直接代入する方法の2スタイルがあります。
4. 組み合わせ回路で作るROM
入力としてアドレスが与えられ、そのアドレスに対応するあらかじめ決められた値dataを出力する回路です。functionを使って記述しますが、always文でも記述可能です。
以下は4ビットアドレス入力、8ビットデータ出力のROM(アドレス0〜15に0の二乗値を格納)の例です。
module rom( addr, data );
input [3:0] addr;
output [7:0] data;
function [7:0] romout;
input [3:0] addr;
begin
case( addr )
0: romout = 0;
1: romout = 1;
2: romout = 4;
3: romout = 9;
4: romout = 16;
5: romout = 25;
6: romout = 36;
7: romout = 49;
8: romout = 64;
9: romout = 81;
10: romout = 100;
11: romout = 121;
12: romout = 144;
13: romout = 169;
14: romout = 196;
15: romout = 225;
default: romout = 8'hxx;
endcase
end
endfunction
assign data = romout( addr );
endmodule
- 入力アドレス信号をcaseの比較値とし、対応する値をfunctionの戻り値
romoutに代入 - assign文で出力
dataへ結果を代入 - ASICメーカーやFPGAメーカーがあらかじめROMブロックを用意している場合がある
- 乗算回路同様、論理合成の結果をみて回路規模が小さくなる方を使用する
5. 修了判定:論理合成結果の確認
この教材にもとから載っている合成例の「ゲート数(回路規模)」を見てみよう🐾
乗算回路(掛け算)がどれだけ巨大な回路になるか、その目で確かめてみてね!
※下の数値とレポートは、この教材にもとから載っている合成例だよ。ツールの版・ライブラリ・設定の記録は残っておらず、元の出典も確認できていない。こちらで再現したものではないので、回路の種類どうしの大小関係を読むための例として見てね。
| 回路 | 使用ファイル | 論理合成結果(ゲート数) |
|---|---|---|
| シフト回路 | shift.v | 54 |
| 一致比較回路 | comp_eq.v | 29 |
| 乗算回路 | multi.v | 615 |
各回路の論理合成レポート詳細
シフト回路(shift)
*************************************
レポート : area
回路 : shift
*************************************
ポート数: 19
ネット数: 49
セル数: 38
セル種類: 8
組み合わせ回路: 54
非組み合わせ回路: 0
合計: 54
一致比較回路(comp_eq)
*************************************
レポート : area
回路 : comp_eq
*************************************
ポート数: 17
ネット数: 27
セル数: 11
セル種類: 3
組み合わせ回路: 29
非組み合わせ回路: 0
合計: 29
乗算回路(multi)
*************************************
レポート : area
回路 : multi
*************************************
ポート数: 33
ネット数: 330
セル数: 306
セル種類: 17
組み合わせ回路: 615
非組み合わせ回路: 0
合計: 615
実習で確かめる、つまずきの補足
加算器の機能と速さを分けて測る
1ビット全加算器は sum = a ^ b ^ cin、cout = (a & b) | (a & cin) | (b & cin)。3入力8通りを確かめると、和と桁上がりを取り違えていないか調べられるよ。
リップルキャリーは前段の桁上がりを次段へ伝えるため、段数が増えると経路が長くなる。先読み方式はgenerate(生成)とpropagate(伝搬)を使ってキャリーを展開するよ。たとえば g0=a0&b0、p0=a0^b0、c1=g0|(p0&c0) だね。ただし入力数や配線も増えるため、方式名だけで最速とは決められない。
回路の中で、入口から出口まで一番時間のかかる経路をクリティカルパスと呼ぶよ。クロックの速さは、いちばん遅いこの経路が間に合うかどうかで決まる。加算器なら、下位から上位へ桁上げが伝わる経路がクリティカルパスになりやすいんだ。どこがその経路になるかは、合成後のタイミング解析レポートで確かめられるよ。
FPGAの + は専用キャリー資源へ対応付けられる場合がある。手書きの先読み式が常に速い/必ず遅い、のどちらでもないよ。同じデバイスと制約で実装し、クリティカルパスと資源使用量を比較しよう。機能シミュレーションがPASSでも周波数の保証にはならないんだ。
パイプラインを増やすと1段の計算量を減らせる一方で、結果までのサイクル数が変わる。データだけでなくvalidも同じ段数だけ遅らせ、期待値の時刻を更新してね。
📝 C3 まとめ: 演算回路の記述と規模比較
| 回路 | 記述方法 | 回路規模の目安 | 注意点 |
|---|---|---|---|
| シフト回路 | assign + << | 中(54ゲート) | ビット数増加で規模増大 |
| 一致比較回路 | assign + == | 小(29ゲート) | 最も軽量 |
| 乗算回路 | assign + * | 大(615ゲート) | ASICやFPGAの専用ブロックと比較すること |
| ※ゲート数は、この教材にもとから載っている合成例の値です。ツールの版・ライブラリ・設定の記録は残っておらず、こちらで再現したものではありません。回路の種類どうしの大小関係を読むための目安として見てください。 | |||
| ROM | function + case | 規模はデータ量次第 | 専用ROMブロックと比較すること |
