program_counter.v
program_counter.vはプログラムカウンタという名前はついていますが一般的なカウンタ回路というものではなく、register_8.vとほとんど同じ構造を持った16ビットサイズのレジスタです。ビットサイズを除くとregister_8.vと異なる点は出力端子が2つあることだけです。
module program_counter(pc_e, rst, pc_in, pc_out, pc_inc_out);
input pc_e;
input rst;
input [15:0] pc_in;
output [15:0] pc_out;
output [15:0] pc_inc_out;
reg [15:0] pc_out;
always @(posedge pc_e or negedge rst) begin
if(rst == 1'b0) begin
pc_out <= 16'b0000000000000000;
end else begin
pc_out <= pc_in;
end
end
assign pc_inc_out = pc_out + 16'b0000000000000001;
endmodule
ソースコードの17行目にあるassign文は現在”pc_out”というレジスタが保持している値に+1した値を、2つ目の出力端子である”pc_inc_out”へ出力する。という意味になります。ここで注目すべきは”pc_out”がレジスタ兼出力端子であるのに対し、”pc_inc_out”は単なる出力端子として定義されているという点です。(”pc_out”はreg宣言されているが”pc_inc_out”はreg宣言されていない。)”pc_inc_out”はレジスタではなく出力端子というただの信号線なのでalways文の中ではなく、always文の外でassign文により信号が代入されています。
”pc_inc_out”にはレジスタ兼出力端子である”pc_out”の保持する値が足し算回路を通して常に+1された状態で出力されています。CPUの設計図上では「PC」の出力が+1されて再び「PC」の入力に接続されるようになっていますが、これは”pc_inc_out”の値を一度外部の回路を通ってから再び”pc_in”の値として入力することを意味しています。こうすることで”pc_out”の値はクロック信号の”pc_e”が立ち上がるたびに+1ずつカウントアップしていく動作が実現します。
alu.v
alu.vはALU(演算装置)の動作を記述するコードなのでいろいろな演算式が出てきますが、最も重要な動作は”opcode”という入力端子によって内部の演算回路を切り替える16行目のcase文の記述の中にあります。
module alu(alu_e, rst, opcode, alu_in_a, alu_in_b, alu_out, flag);
input alu_e;
input rst;
input [3:0] opcode;
input [7:0] alu_in_a;
input [7:0] alu_in_b;
output [7:0] alu_out;
output flag;
reg[7:0] alu_out;
reg flag;
always @(posedge alu_e or negedge rst) begin
if(!rst)begin
alu_out <= 8'b00000000;
end else begin
case(opcode)
4'b0000: alu_out <= alu_in_a; // output <- input A
4'b0001: begin
{flag, alu_out} <= alu_in_a + alu_in_b; // ADD
end
4'b0010: begin
alu_out <= alu_in_a - alu_in_b; // SUB
if( (alu_in_a - alu_in_b) == 8'b00000000 )
flag <= 1'b1;
else
flag <= 1'b0;
end
4'b0011: begin
alu_out <= alu_in_a & alu_in_b; // AND
if( (alu_in_a & alu_in_b) == 8'b00000000 )
flag <= 1'b1;
else
flag <= 1'b0;
end
4'b0100: begin
alu_out <= alu_in_a | alu_in_b; // OR
if( (alu_in_a | alu_in_b) == 8'b00000000 )
flag <= 1'b1;
else
flag <= 1'b0;
end
4'b0101: begin
alu_out <= ~alu_in_b; // NOT
if( (~alu_in_b) == 8'b00000000 )
flag <= 1'b1;
else
flag <= 1'b0;
end
4'b0110: begin
{flag, alu_out} <= {1'b0, alu_in_a} << alu_in_b; // SL
end
4'b0111: begin
{alu_out, flag} <= {alu_in_a, 1'b0} >> alu_in_b; // SR
end
4'b1000: alu_out <= alu_in_b; // output <- input B
default: alu_out <= 8'b00000000;
endcase
end
end
endmodule
case文は”case”に続く括弧の中に入れた信号の値によって実行される文を切り替えるという機能を持っています。数値の後ろに「:」が付いた記述が並んでいますが、この数値とcase文の括弧の中に入れた信号の値が一致した時にその値の「:」以降に記述された文が実行されます。今回の例では”case”に続く括弧の中に”opcode”という4ビットの入力端子が入っていますので、例えば”opcode”が4ビットの2進数表現で0100になった場合にはalu_out <= alu_in_a | alu_in_b;の記述とその後に続くif文が実行されます。(”// OR”のような「/」が続けて2つ記述された部分はコメントアウトと言って”//”以降の記述がVerilog HDLのコードとして認識されなくなります。ソースコード上にメモなどを残したい時に使用されます。)if文では”alu_out”が0になった時にフラグを「1」にする処理が記述されています。
case文の中に出てくる演算子はそれぞれ次のような意味の演算子です。”+”は足し算、”-”は引き算、”&”はAND、”|”はOR、”~”はNOT、”<<”は左シフト、”>>”は右シフト。”<<”と”>>”は右側に書かれた数値分(ここでは”alu_in_b”の値)のビット数だけシフトします。
足し算回路と左シフト、右シフト回路の記述には信号線を束ねるときに使用される波括弧が出てきます。波括弧は連接演算子と呼ばれるもので、複数の信号を1つの信号に束ねる役割を果たします。括弧の中にコンマ区切りで記入された信号は、各信号のビットサイズを足しただけのビットサイズを持つ1つの信号として解釈されます。(例えば3ビットの信号と5ビットの信号を波括弧で束ねた場合、その信号のサイズは8ビットになります。)連結された後の各信号の並びは左側に記述された信号が上位ビット、右側に記述された信号が下位ビットという関係になります。
足し算回路の記述では連接演算子によって”alu_out”の信号に”flag”という信号が連結された9ビットの信号が作られています。”flag”は”alu_out”の最上位ビットの1つ左側のビットとして連結され、足し算の結果で”alu_out”に桁あふれが発生した場合に”flag”が1になるようになっています。
左シフト回路の記述でも”flag”は”alu_out”の最上位ビットの1つ左側のビットとして連結され、左シフトの結果で桁あふれが発生した場合に”flag”が1になるようになっています。なお”alu_in_b”の値が2以上の場合で”flag”ビットよりも左側へ桁あふれが発生した場合にはそのビットは無効となるためフラグの判定対象とはなりません。
左シフト回路の記述で”alu_in_a”の最上位ビットの左側に0が1ビット分追加されているのは左シフト演算の結果として最上位ビットの1つ左側のビットを残すためです。桁あふれが発生した場合に”alu_in_a”が8ビットのままだと桁あふれを起こしたビットは捨てられてしまいます。
右シフト回路の記述では”flag”は”alu_out”の最下位ビットの1つ右側のビットとして連結され、右シフトの結果で桁あふれが発生した場合に”flag”が1になるようになっています。なお”alu_in_b”の値が2以上の場合で”flag”ビットよりも右側へ桁あふれが発生した場合にはそのビットは無効となるためフラグの判定対象とはなりません。
右シフト回路の記述で”alu_in_a”の最下位ビットの右側に0が1ビット分追加されているのは右シフト演算の結果として最下位ビットの1つ右側のビットを残すためです。桁あふれが発生した場合に”alu_in_a”が8ビットのままだと桁あふれを起こしたビットは捨てられてしまいます。
スポンサーリンク
case文の「数値:」以降の実行文は処理の内容が複数行で記述される場合は”begin”と”end”で囲います。1行で記述できる場合は”begin”と”end”は省略できます。
そしてこのコードにはソフトウェアを記述する言語とハードウェアを記述する言語の重要な違いが表れる箇所があります。例えば以下のコードです。
【コード1】
4'b0100: begin
alu_out <= alu_in_a | alu_in_b; // OR
if( (alu_in_a | alu_in_b) == 8'b00000000 )
zero_flag <= 1'b1;
else
zero_flag <= 1'b0;
end
ソフトウェア開発の経験がある方からすると、上記のコードは以下のような書き方でも良いのではないかと思われるかもしれません。
【コード2】
4'b0100: begin
alu_out <= alu_in_a | alu_in_b; // OR
if( alu_out == 8'b00000000 )
zero_flag <= 1'b1;
else
zero_flag <= 1'b0;
end
コード1の中でif文の括弧の中に記述されている条件式の左辺は”alu_out”に代入されている内容と同じであるのだから、コード1とコード2はどちらも同じ意味になるように見えます。しかし、ここにハードウェア記述言語で注意しなければならない違いがあります。
ハードウェア記述言語では上の文から順番に処理が実行されていくという概念は基本的にはないと考えた方がわかりやすいです。例えば上記のようなコードでは”alu_out”への代入とif文の処理は同じタイミングで実行されます。(”alu_e”信号の立ち上がりの瞬間に同時に実行される。)
”alu_out”への代入とif文の処理が同じタイミングで実行されるということになると、if文の処理が実行される瞬間にはまだ”alu_out”への代入は完了していないと見る必要があります。なのでハードウェア記述言語ではコード1とコード2は同じ意味にはなりません。もしコード2のような書き方をしてしまうとif文の判断は1つ前のタイミングで”alu_out”に代入されていた値をもとに実行されることになります。
このようにハードウェア記述言語は見た目はソフトウェアの言語と似ているのですが、処理が実行されるタイミングに関しては大きな違いがあるため注意が必要です。(精確には前述のブロッキング代入文を使うことによってソフトウェア的な記述も可能ですが、ここでは説明を省略させていただきます。)
”opcode”と比較する数値が並んだ最後に”default:”という記述が出てきますが、これは括弧の中に入れた信号の数値(ここでは”opcode”)が比較対象として並んでいる数値の中に無かった場合に実行される文です。例えばここでは”opcode”が4ビット2進数表現で1001になった時の処理は記述されていません。このような定義されていない入力があった場合には”default:”以降の処理(ここではalu_out <= 8’b00000000;)が実行されます。
case文の最後は”endcase”という記述で終わる決まりになっています。
<戻る 次へ>