Verilog HDLにおける演算記述の罠

最近はまった乗算の罠について

ビット幅の罠

まずは以下のVerilog記述を見ていただきたい。a*b と c、いずれもaとbの乗算を行っている。どのように出力されるでしょうか?

module test1;
reg [1:0] a = 2'd2;
reg [1:0] b = 2'd3;
reg [3:0] c;
initial begin
  c = a * b;
  $display( "a*b = %d", a*b );
  $display( "c = %d", c );
  $finish;
end
endmodule

結果は以下の通り。

a*b = 2
c =  6

a*b と直接記述した方はビット幅が削られているように見える。なぜなのか?

ビット幅の規則

SystemVerilogのLRMは以下からダウンロードできる。

IEEE Standard Association - IEEE Get Program

これの 11.6.1 Rules for expression bit lengths によると、式のビット幅は2種類あるとされる。

  • self-determined expression: 式そのものによってビット幅が決定される
  • context-determined expression: 文全体によってビット幅が決定される

前述の例では、「a*b」がself-determined expressions、「c = a*b;」 が context-determined expressionとなっている。self-determined expressionsでは、Table 11-21 Bit lengths resulting from self-determined expressionsによりビット幅が決定される。算術演算の場合、オペランドの大きい方のビット幅が演算結果のビット幅となる。代入文では、context-determined expressionとなるので演算結果のビット幅は左辺のビット幅となる。

したがって、a*bではビット幅2の演算と見なされ、a*bの結果6 "0110"のうち下位2ビットの"10"が演算結果となってしまう。

ビット幅の規則に関する注意点

11.6.2 Example of expression bit-length problem に興味深いことが記されている。以下の記述の場合、answerは期待通りに計算されるであろうか?

wire [15:0] a, b, answer;
assign answer = (a + b) >> 1;

何を期待通りとするかは判断の分かれるところだが、a+bの演算結果で桁上げが発生し一時的に17ビットとなってしまう場合、この演算結果は桁上げが捨てられてしまう。つまり、演算結果の最上位ビット answer[15]は常に0となる。なぜかというと、この文はcontext-determined expressionであるから、中間の演算結果は左辺answerの16ビットとなる。したがって、a+bが桁上げによって17ビットとなってしまった場合、最上位ビットは捨てられてしまう。これを防ぐためには以下のようにself-determined expressionsにより17ビットの演算となるようにしなければならない。

assign answer = ({1'b0, a} + b) >> 1;

上記の実証コードを以下に記載する。

module test3;
reg [2:0] a = 7;
reg [2:0] b = 1;
reg [2:0] c;
initial begin
  c = (a+b) >> 1;
  $display( "c = %d", c );
  c = ({1'b0,a}+b) >> 1;
  $display( "c = %d", c );
  $finish;
end
endmodule

実行結果は以下の通り。

c = 0
c = 4

また、連結演算 {} の中ではself-determined expressionsとなってしまう。したがって、以下の例はself-determined expressionsとなる。

module test1_2;
reg [1:0] a = 2'd2;
reg [1:0] b = 2'd3;
reg [3:0] c;
initial begin
  c = {a * b};
  $display( "c = %d", c );
  $finish;
end
endmodule

実行結果

c =  2

符号付き演算の罠

符号付きの数と符号無しの数を乗算したいとする。以下のような記述は期待通りの結果が得られるであろうか?

module test2;
reg [2:0] a = -2;
reg [2:0] b = 3;
reg [5:0] c;
initial begin
  c = $signed(a) * b;
  $display( "c = %d", $signed(c) );
  $finish;
end
endmodule

実行結果は以下のようになる。

c =  18

符号付き演算の規則

11.8.1 Rules for expression typesの最後の2つを見ると以下のような記述がある。

  • オペランドの中に符号無し数が含まれる場合、演算結果も符号無しになる
  • オペランド全てが符号付き数の場合、演算結果も符号付きになる(例外はあるが)

つまり、前述の乗算は符号無しオペランドが含まれるため、符号無しの乗算となってしまっていた。したがって、"110" * "011" = "010010" となり、18という結果となった。

これを回避する一つの方法として、符号拡張がある。

module test2_1;
reg [2:0] a = -2;
reg [2:0] b = 3;
reg [5:0] c;
initial begin
  c = $signed({ {3{a[2]}}, a} ) * { {3{b[2]}}, b };
  $display( "c = %d", $signed(c) );
  $finish;
end
endmodule

実行結果は以下のようになり、一見良さそうに見える。

c =  -6

しかし、この記法では、シミュレーションでは良くても論理合成まで含めて考えると良くない。論理合成の処理系によっては、この記述は6ビット同士の符号無し乗算と認識される。したがって、たとえばターゲットデバイスFPGADSPブロックが18ビット同士の乗算をサポートしており、a,bのビット幅が16、cのビット幅が32の場合、上記の記述ではDSPブロックを2つ以上消費してしまうことになってしまう。これは良くない設計である。

ベストな記述は以下のように符号付き数同士の乗算としてしまうことである。

module test2_2;
reg [2:0] a = -2;
reg [2:0] b = 3;
reg [5:0] c;
initial begin
  c = $signed( a ) * $signed( { 1'b0, b } );
  $display( "c = %d", $signed(c) );
  $finish;
end
endmodule

実行結果は以下のようになる。

c =  -6

このように記述すれば、論理合成処理系も乗算のビット幅を誤って認識することはない。

まとめ

演算のビット幅、符号付き演算のVerilog HDLによるRTL記述には落とし穴がある。筆者のおすすめとしては、落とし穴にはまらないようにするには以下のような方針で記述すると良さそうだ。

  • 乗算以外の場合、右辺のビット幅は桁上げに気をつけて基本的に左辺のビット幅にそろえてから演算する。
  • 乗算の場合、右辺は最小限のビット幅とする(合成処理系でのロスを防ぐため)。演算結果が符号付きの場合は両オペランドをsignedに変換してから演算する。