加算器 その1
この記事はkivantium Advent Calendarの3日目です。
昨日はNANDゲートから任意のゲートを作れることを確認しました。
ここからは何回かに分けて四則演算などの基本的な計算を行う回路の実現方法を書いていきます。
四則演算は組み合わせ回路として書けるので、NANDであらゆるゲートを作れる以上実現できることは明らかなのですが、使用するトランジスタの数を減らしたり計算を高速にしたりするためにいろいろな方法が考えられています。
2進数による正の整数の表現
デジタル回路では電圧のH/Lに対応した1/0の2値で全ての情報を表します。2値で数字を表すために2進数を使います。
を0または1として、
のように並べた数字が
という値を表すように取り決めるのが2進数です。
2進数での桁のことをbit (BInary digiT)と呼び、2進数で8桁の数字を「8bitの数」のようにいいます。
4bitの場合の2進数と10進数の対応関係を書いておきます。
2進数 | 10進数 | 2進数 | 10進数 | |
0000 | 0 | 1000 | 8 | |
0001 | 1 | 1001 | 9 | |
0010 | 2 | 1010 | 10 | |
0011 | 3 | 1011 | 11 | |
0100 | 4 | 1100 | 12 | |
0101 | 5 | 1101 | 13 | |
0110 | 6 | 1110 | 14 | |
0111 | 7 | 1111 | 15 |
1bit 半加算器 (HA, half adder)
1bit半加算器は1bitの数の足し算を行う回路です。入力Aと入力Bが与えられたときに和Sと繰り上がりC (carry out)を出力します。真理値表は
A | B | C | S |
0 | 0 | 0 | 0 |
0 | 1 | 0 | 1 |
1 | 0 | 0 | 1 |
1 | 1 | 1 | 0 |
のようになります。
半加算器を2入力2出力のゲートと考えれば、NANDゲートをうまく組み合わせて作れます。
実際にはNANDゲートだけで作るのは面倒なので一例として以下のように作ります。
この回路図ではXORゲートが使われています。XORゲートは
A | B | Y |
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
という真理値表を持つゲートです。XORゲートの記号は
です。
1bit 全加算器 (FA, full adder)
全加算器は繰り上がりも含めて加算を行う回路です。入力をA・Bと前の桁の繰り上がりCinの3本、出力を和Sと繰り上がりCoutの2本とすると、真理値表は
A | B | Cin | Cout | S |
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 0 | 1 |
1 | 0 | 0 | 0 | 1 |
1 | 1 | 0 | 1 | 0 |
0 | 0 | 1 | 0 | 1 |
0 | 1 | 1 | 1 | 0 |
1 | 0 | 1 | 1 | 0 |
1 | 1 | 1 | 1 | 1 |
のようになります。
これを実現する回路は一例として
があります。
nbit 全加算器
1bitの加算器をいくつも集めることで複数bit同士の足し算を行う加算器を作ることができます。nbitの入力x, yの和sを計算する回路は例えば以下のようになります。(FAは全加算器を表しています)
x0はxの第0bit、x1は第1bit……のように入力の各bitを表しています。(全体のCinは0に固定しておきます)
このように下の桁の全加算器のCoutを上の桁の全加算器のCinにつないで作る加算器をリップルキャリーアダーといいます。
この回路でも結果は正しいのですが、例えば64bitの足し算を行おうとすると全加算器を直列に64個つなぐことになります。
全加算器はゲートをつないで作っていますが、そのゲートはトランジスタをつないで作っています。
トランジスタは電圧を加えてからスイッチングが起こるまでに若干の遅れがあります。そのためゲートの入力電圧が変わってから出力が変わるまでにゲート遅延と呼ばれる遅れが発生します。(例えばNANDゲートのICである74HC00では10nsくらいです)これは人間的スケールでは一瞬なのですが、2GHzなどの超高速で動くCPUの世界ではこのゲート遅延は非常に問題になります。(1GHzのクロックは1ns間隔です)
次のクロックが起こるまでに演算を終わらせようと思うと、なるべくゲートを直列につなぐ数を減らす必要があります。このために高速な何種類も加算器が提案されています。次回はキャリールックアヘッドアダーと呼ばれる高速化した加算器を紹介します。
SystemVerilogでの実装
ここまで紹介した回路をSystemVerilogというハードウェア記述言語で書いてみます。SystemVerilogの基本文法については以前の記事を参照してください。
kivantium.hateblo.jp
実行環境
FPGAなどで実際に回路を動かすのは大変なので、この記事ではシミュレータで動作を確認します。
シミュレータには何種類かあるので好きなものを使ってください。
Icarus Verilog
オープンソースのシミュレータです。
インストール方法はInstallation Guideを見てください。
SystemVerilogはUbuntu 14.04のレポジトリにあるバージョンでは動かなかったのでソースコードからコンパイルしました。g++, gcc, bison, flex, gperfを入れておけばいつものsh autoconf.sh && ./configure && make && make install
で環境構築ができました。
また、SystemVerilogを動かすには
iverilog -g2012 -o test test.sv vvp test
のようにコンパイル時にオプションを指定する必要がありました。
Vivado
XilinxのページからVivadoを落としてくればシミュレータがついてきます。
半加算器
HAモジュールは半加算器を紹介した回路図の通りに作っています。
テストベンチでは真理値表のようなものを出力しています。
module testbench; logic a, b, c, s; HA adder(.A(a), .B(b), .C(c), .S(s)); initial begin $display("A B C S"); $monitor("%b %b -> %b %b", a, b, c, s); a = 0; b = 0; #10; a = 0; b = 1; #10; a = 1; b = 0; #10; a = 1; b = 1; #10; $finish; end endmodule module HA( input A, B, output S, C ); assign S = A ^ B; assign C = A & B; endmodule
結果は
A B C S 0 0 -> 0 0 0 1 -> 0 1 1 0 -> 0 1 1 1 -> 1 0
のようになり、期待通り動くことが分かりました。
全加算器
半加算器と同様です。
module testbench; logic a, b, cin, s, cout; FA adder(.A(a), .B(b), .Cin(cin), .S(s), .Cout(cout)); initial begin $display("A B Ci Co S"); $monitor("%b %b %b -> %b %b", a, b, cin, cout, s); a = 0; b = 0; cin = 0; #10; a = 0; b = 1; cin = 0; #10; a = 1; b = 0; cin = 0; #10; a = 1; b = 1; cin = 0; #10; a = 0; b = 0; cin = 1; #10; a = 0; b = 1; cin = 1; #10; a = 1; b = 0; cin = 1; #10; a = 1; b = 1; cin = 1; #10; $finish; end endmodule module FA( input A, B, Cin, output S, Cout ); assign S = (A ^ B) ^ Cin; assign Cout = ((A ^ B) & Cin) | (A & B); endmodule
結果は以下の通りです。
A B Ci Co S 0 0 0 -> 0 0 0 1 0 -> 0 1 1 0 0 -> 0 1 1 1 0 -> 1 0 0 0 1 -> 0 1 0 1 1 -> 1 0 1 0 1 -> 1 0 1 1 1 -> 1 1
4bit リップルキャリーアダー
全加算器を4つつなげてadder_4bitというモジュールを作っています。
module testbench; logic [3:0] a, b, result, answer; logic [4:0] result5; logic carry; int i, j; adder_4bit adder(a, b, result, carry); assign answer = a+b; assign result5 = {carry, result}; // result5の最上位bitをcarry, 3-0bitをresultにする initial begin $monitor("%04b+%04b -> %05b (%d+%d=%d)", a, b, result5, a, b, result5); for (i = 0; i<=15; i++) begin for (j = 0; j<=15; j++) begin a = i; b = j; #1; if(result != answer) $display("wrong answer!"); end end $finish; end endmodule module FA( input A, B, Cin, output S, Cout ); assign S = (A ^ B) ^ Cin; assign Cout = ((A ^ B) & Cin) | (A & B); endmodule module adder_4bit( input [3:0] A, B, output [3:0] S, output C ); logic [3:0] carry; FA fa0(A[0], B[0], 1'b0, S[0], carry[0]); FA fa1(A[1], B[1], carry[0], S[1], carry[1]); FA fa2(A[2], B[2], carry[1], S[2], carry[2]); FA fa3(A[3], B[3], carry[2], S[3], C); endmodule
結果は
0000+0000 -> 00000 ( 0+ 0= 0) 0000+0001 -> 00001 ( 0+ 1= 1) 0000+0010 -> 00010 ( 0+ 2= 2) 0000+0011 -> 00011 ( 0+ 3= 3) (中略) 1111+1101 -> 11100 (15+13=28) 1111+1110 -> 11101 (15+14=29) 1111+1111 -> 11110 (15+15=30)
のようになりました。
4bitリップルキャリーアダーはgenerate文を使うと以下のようにも書けます。
module adder_4bit #(parameter SIZE = 4) ( input [SIZE-1:0] A, B, output [SIZE-1:0] S, output C ); logic [SIZE-2:0] carry; FA fa0(A[0], B[0], 1'b0, S[0], carry[0]); genvar i; generate for(i=1; i<SIZE-1; i=i+1) begin: GenerateAdder FA fa(A[i], B[i], carry[i-1], S[i], carry[i]); end endgenerate FA fa_msb(A[SIZE-1], B[SIZE-1], carry[SIZE-2], S[SIZE-1], C); endmodule
SIZEを変えればより大きい加算器も作れます。
参考ページ
- Adder (Wikipedia) 図はここから引用しました