演算回路を書く

📌 このページの概要と目標

概要: シフトや乗算、大小比較など演算回路の記述を学び、論理合成して回路規模を確認する。

目標:

  • 以下の回路の記述ができる
    • 組み合わせ回路によるシフト回路
    • 乗算回路
    • 大小比較回路/一致比較回路
    • 組み合わせ回路で作る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でも、動作周波数の保証にはならないんだ。

わざと間違えて、テストの働きも確かめる

2026年10月26日 Windows / Icarus Verilog 13.0での実行結果
壊した場所結果
全加算器の 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が入ります。

C3 図1:8ビットシフト回路のブロック図。din 8bitとsft 3bitがshiftに入り、dout 8bitが出る。矢印のビット幅は8、3、8。
C3 図1:8ビットシフト回路のブロック図。横に長い図はスクロールして確認できます。

シフト動作の例

信号値(2進)値(16進)
din1 0 1 1 0 1 0 18'hb5
sft3(3ビット左シフト)
dout1 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ビット幅が必要です。

C3 図2:4ビット乗算回路のブロック図。a 4bitとb 4bitがmultiに入り、8ビットのq 8bitが出る。
C3 図2:4ビット乗算回路のブロック図。横に長い図はスクロールして確認できます。
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行で記述できます。

C3 図3:一致比較回路と大小比較回路の2つを示す図。一致側ではa 8bitとb 8bitがcomp_EQに入りa_eq_b 1bitが出る。大小側ではa 8bitとb 8bitがcomp_GTに入りa_gt_b 1bitが出る。
C3 図3:一致比較回路と大小比較回路の2つを示す図。横に長い図はスクロールして確認できます。

大小比較回路(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
💡 記述の構造は同じ 大小比較回路と一致比較回路は、assign文の内容以外は同じ構造で記述できます。条件演算子(? :)を使う方法と、等号演算結果を直接代入する方法の2スタイルがあります。

4. 組み合わせ回路で作るROM

入力としてアドレスが与えられ、そのアドレスに対応するあらかじめ決められた値dataを出力する回路です。functionを使って記述しますが、always文でも記述可能です。

以下は4ビットアドレス入力、8ビットデータ出力のROM(アドレス0〜15に0の二乗値を格納)の例です。

C3 図4:ROMモジュールのブロック図。4ビットのaddr 4bitがromに入り、8ビットのdata 8bitが出る。
C3 図4:ROMモジュールのブロック図。横に長い図はスクロールして確認できます。
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
💡 ROMの回路規模について
  • 入力アドレス信号をcaseの比較値とし、対応する値をfunctionの戻り値romoutに代入
  • assign文で出力dataへ結果を代入
  • ASICメーカーやFPGAメーカーがあらかじめROMブロックを用意している場合がある
  • 乗算回路同様、論理合成の結果をみて回路規模が小さくなる方を使用する

5. 修了判定:論理合成結果の確認

もふねこ

この教材にもとから載っている合成例の「ゲート数(回路規模)」を見てみよう🐾
乗算回路(掛け算)がどれだけ巨大な回路になるか、その目で確かめてみてね!
※下の数値とレポートは、この教材にもとから載っている合成例だよ。ツールの版・ライブラリ・設定の記録は残っておらず、元の出典も確認できていない。こちらで再現したものではないので、回路の種類どうしの大小関係を読むための例として見てね。

⚠️ 修了判定 設問:8ビットのシフト回路・一致比較回路・乗算回路を論理合成ツールで合成し、ゲート数(回路規模)の大小関係を確認する。
回路使用ファイル論理合成結果(ゲート数)
シフト回路shift.v54
一致比較回路comp_eq.v29
乗算回路multi.v615

各回路の論理合成レポート詳細

シフト回路(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の専用ブロックと比較すること
※ゲート数は、この教材にもとから載っている合成例の値です。ツールの版・ライブラリ・設定の記録は残っておらず、こちらで再現したものではありません。回路の種類どうしの大小関係を読むための目安として見てください。
ROMfunction + case規模はデータ量次第専用ROMブロックと比較すること