CPUの創り方をもとにverilog-hdlでシミュレーションをしてみた

CPUの創り方をもとにverilog-hdlでシミュレーションをしてみた

CPUの創り方 にのっているキッチンタイマー(時間固定)のCPUをverilog-hdlでシミュレーションしてみました。実装についてはこちらに作られていたのがありましたので、これをもとに確認しました。

環境構築(QuartusⅡ)

環境を準備するのが大変だと思うのですが、自分はQuartusⅡ Web Editionを入れて確認しました。QuartusⅡ Web Editionはこちらのページの"ソフトウエアを選択"からダウンロードできるかと思います。

新規プロジェクト作成

QuartusⅡインストールまで完了して新規プロジェクトの作成では以下を選択します。 "File" -> "New Project Wizard..." 途中でインストール先やプロジェクトタイプや使用するデバイス(FPGA)について聞かれますが、verilog-hdlのシミュレーションをしたいだけであればインストール先を指定後"Finish"を選択して大丈夫のはずです。

新規verilogファイル作成

新規プロジェクト作成後ファイルを追加する場合は以下を選択します。 "File" -> "New..." -> "Design Files" -> "Verilog HDL File" -> "OK"

4bitレジスタの作成

今回は4bitのcpuを作るのですが、この4bitはcpuが保持できる状態のサイズになっており保持するためにはレジスタを使います。4bitのcpuであればレジスタが保持できるデータサイズが4bitになりまして、cpuではレジスタを複数保持しています。

  • RegisterFile.v
module RegisterFile (
  input   clk_cpu,
  input   reset,
  input   load,
  input   [3:0] dat_in,
  output  [3:0] dat_out
);

  reg [3:0] register;
  
  always @(posedge clk_cpu or posedge reset) begin
    if (reset) begin
      register <= 4'd0;
    end else if (load) begin
      register <= dat_in;
    end
  end
  
  assign dat_out = register;

endmodule

4bitレジスタの実装は↑のようになるのですが、やっている内容としてはクロックのタイミングでresetが立っていたら0を出力、loadが立っていたら入力値をそのまま返す、それ以外は依然の出力値をそのまま返すといった内容になっています。verilogだと簡単にかけてよいのですが、データ保持のためには実際の回路としてはフリップフロップを使っているかと思います。

これだけだと何に使えるのかイメージがわかないかもしれませんが、例えばアセンブラ言語のmovは値のコピーを行うのですがloadを立ててコピーする値をdat_inに渡すことでmovを実現することができます。またcpuが実行する命令のアドレスを保持するためのプログラムカウンタはレジスタ上に保存しておきクロックのタイミングでdat_outの内容を+1してdat_inに渡す、それからjnc命令で実行する命令のアドレスを変更する場合はdat_inに渡す内容をジャンプ先に帰ることで実現できます。

では実際にレジスタの動作を確認したいと思うのですが、動作確認用に以下のファイルを追加します。

  • RegisterFileTest.v
`include "Defines.v"

module RegisterFileTest();
  
reg clk_cpu;
reg reset;
reg load;
reg [3:0] dat_in;
wire [3:0] dat_out;
integer i;

RegisterFile Register0(
  .clk_cpu(clk_cpu),
  .reset(reset),
  .load(load),
  .dat_in(dat_in),
  .dat_out(dat_out)
);

// clock
initial
begin
    clk_cpu = 0;
    forever #`HCYCL clk_cpu = !clk_cpu;
end

// test
initial begin
  
  // reset
  repeat(5) @(posedge clk_cpu);
  test_reset();
  
  // load sequential values
  repeat(5) @(posedge clk_cpu);
  dat_in = 0;
  repeat(20) begin
    test_load(dat_in + 1);
  end
  
  // reset
  repeat(5) @(posedge clk_cpu);
  test_reset();
end

// tasks
task test_reset; 
begin
  reset = 1;
  load = 0;
  repeat(5) @(posedge clk_cpu)
    if (dat_out != 4'd0)
      $display("test_reset_register_file: dat_out is not zero");
  reset = 0;
end
endtask

task test_load; 
input [3:0] dat_next;
begin
  @(negedge clk_cpu) begin
    load = 1;
    dat_in = dat_next;
  end
  @(negedge clk_cpu) begin
    load = 0;
    #`HSTRB;
    if (dat_out != dat_in)
      $display("test_load: dat_out is not equal to dat_in");
  end
end
endtask

endmodule
  • Defines.v
`timescale 1 ns/ 1 ns

`define HCYCL 10
`define HSTRB 8

動作検証としてレジスタへの入力や出力のための変数を定義して渡しています。以下の部分では0から20までカウントしてtest_loadに渡しています。test_load内ではloadを1にしてdat_inにカウントした結果を渡しているのでレジスタで保持している値もカウントアップした結果になっているはずです。

  dat_in = 0;
  repeat(20) begin
    test_load(dat_in + 1);
  end

動作を確認する場合はQuartusⅡのメニューからStart Compilationをクリックします。シミュレーション対象のメインのモジュールを選択するにはHierarchyの一覧の一番上の項目を右クリックし"Settings..." -> "EDA Tool Settings" -> "Simulation" -> "Test Benchs..." -> "New" と進み、、テスト対象のファイルを追加しTestbench nameを入力しOKをクリックします。

あとはコンパイルに成功した後RTL Simulationをクリックしたら別ウィンドウでシミュレーション用のウィンドウが立ち上がります。

ウィンドウを立ち上げた直後は波形が出力されないのですが"Run -All" をくりっくしてから "Stop" をクリックすると表示されるようになります。 f:id:steavevaivai:20190819153834p:plain

シミュレーション結果よりdat_outがdat_inを追従していることがわかります。

演算装置(ALU)の作成

演算装置といっても今回は桁上がりがある加算器(全加算器)の機能のみで減算や乗算には対応していません。

実装は以下になります。

module Alu (
  input   clk,
  input   reset,
  input   [3:0] data_0_in,
  input   [3:0] data_1_in,
  output  [3:0] data_out,
  output  carry_out
);

  // alu
  wire [4:0] result;
  assign result = data_0_in + data_1_in;
  assign data_out = result[3:0];
  
  // carry flag
  reg carry;
  always @(posedge clk or posedge reset) begin
    if (reset)
      carry <= 0;
    else
      carry <= result[4];
  end
  assign carry_out = carry;

endmodule

data_0_inとdata_1_inが加算対象で結果がdata_outになり、桁上がりが発生したかどうかはcarry_outで分かるようになっています。 このaluを使うことでレジスタの値を加算することができるようになります。

以下のモジュールでaluのシミュレーションをしてみます。 - AluTest.v

`include "Defines.v"

module AluTest();
  
reg clk_cpu;
reg reset;
reg load;
reg [3:0] data_0_in;
reg [3:0] data_1_in;
wire [3:0] dat_out;
wire carry_out;

Alu Alu0(
  .clk(clk),
  .reset(reset),
  .data_0_in(data_0_in),
  .data_1_in(data_1_in),
  .data_out(dat_out),
  .carry_out(carry_out)
);

// clock
initial
begin
    clk_cpu = 0;
    forever #`HCYCL clk_cpu = !clk_cpu;
end

// test
initial begin
  
  // reset
  repeat(5) @(posedge clk_cpu);
  test_reset();
  
  // load sequential values
  repeat(5) @(posedge clk_cpu);
  
  data_0_in = 1;
  data_1_in = 2;
  repeat(5) @(posedge clk_cpu)
  if (dat_out != 3 || carry_out != 0)
      $display("test_load: dat_out is not equal to dat_in");
  
  
  // reset
  repeat(5) @(posedge clk_cpu);
  test_reset();
end

// tasks
task test_reset; 
begin
  reset = 1;
  load = 0;
  data_0_in = 0;
  data_1_in = 0;
  repeat(5) @(posedge clk_cpu)
    if (dat_out != 4'd0)
      $display("test_reset_register_file: dat_out is not zero");
  reset = 0;
end
endtask


endmodule

1+2をしているのでaluの出力結果が3になっているはずです。実際に確認すると以下のようになり、確かに3になっていることが確認できます。 f:id:steavevaivai:20190819153859p:plain

レジスタと演算装置を組み合わせてみる

レジスタと演算装置を組み合わせてmov命令やadd命令に対応したCPUを作ってみます。できることがこれだけなのでCPUに分類できるかどうか微妙ですが

  • CPU_Proto.v
`include "Defines.v"

module CPU_Proto (
  input clk_cpu,
  input reset,
  input select_a,
  input select_b,
  input load_a,
  input load_b,
  input load_pc,
  input [3:0] im,
  output [3:0] pc
);

  // program counter
  reg [3:0] next_pc;
  wire [3:0] alu_data_out;
  RegisterFile RegisterPC(
    .clk_cpu(clk_cpu), 
    .reset(reset), 
    .load(clk_cpu), 
    .dat_in(next_pc), 
    .dat_out(pc)
  );
  always @(*) begin
    if (load_pc)
      next_pc <= alu_data_out;
    else
      next_pc <= pc + 4'd1;
  end
  
  // register A
  wire reg_a_load;
  wire [3:0] reg_a_out;
  RegisterFile RegisterFileA(
    .clk_cpu(clk_cpu), 
    .reset(reset), 
    .load(load_a), 
    .dat_in(alu_data_out), 
    .dat_out(reg_a_out)
  );
  
  // register B
  wire [3:0] reg_b_out;
  RegisterFile RegisterFileB(
    .clk_cpu(clk_cpu), 
    .reset(reset), 
    .load(load_b), 
    .dat_in(alu_data_out), 
    .dat_out(reg_b_out)
  );
  
  
  // ALU
  wire [3:0] alu_data_0_in;
  assign alu_data_0_in =
    select_a == 1 ? reg_a_out :
    select_b == 1 ? reg_b_out :
    4'd0;
  wire [3:0] alu_data_1_in;
  wire alu_carry_out;
  Alu Alu0(
    .clk(clk_cpu),
    .reset(reset),
    .data_0_in(alu_data_0_in),
    .data_1_in(im),
    .data_out(alu_data_out),
    .carry_out(alu_carry_out)
  );
   
endmodule

RegisterPCはプログラムカウンタになります。本来であればプログラムカウンタが命令のアドレスを保持しておりそれをもとに命令を取り出すとかなのですが、今回はカウンターくらいの役割しか果たしていないです。レジスタとしてはRegisterFileAとRegisterFileBの2つがあり、aluはAlu0が一つあります。 レジスタとaluの接続として、レジスタA,レジスタBの出力のどちらかとcpuへの入力imをaluに渡しています、それからaluの出力alu_data_outを2つのレジスタの入力としています。 この時レジスタA,レジスタBの出力のどちらかをaluの入力とするかについてはselect_a, select_bの値で判定しています。

cpuへの入力 select_a, select_b, load_a, load_b, load_pc, imによりmovとaddの命令を実行できるようにしています。例えばレジスタaの値を3に変更したい場合はload_a=1, im = 3で他は0とすることで実現でき、レジスタbの値をレジスタaの値に2加算した結果にする場合はselect_a=1, load_b=1, im=2,他は0とすることで実現できます。

これの動作検証として以下でシミュレーションしてみます。

`include "Defines.v"

module CPU_ProtoTest();
  
reg clk_cpu;
reg reset;
reg cpu_proto_select_a;
reg cpu_proto_select_b;
reg cpu_proto_load_a;
reg cpu_proto_load_b;
reg cpu_proto_load_pc;
reg [3:0] cpu_proto_im;
wire [3:0] cpu_proto_pc;
CPU_Proto CPU_Proto0 (
  .clk_cpu(clk_cpu),
  .reset(reset),
  .select_a(cpu_proto_select_a),
  .select_b(cpu_proto_select_b),
  .load_a(cpu_proto_load_a),
  .load_b(cpu_proto_load_b),
  .load_pc(cpu_proto_load_pc),
  .im(cpu_proto_im),
  .pc(cpu_proto_pc)
);

// clock
initial
begin
    clk_cpu = 0;
    forever #`HCYCL clk_cpu = !clk_cpu;
end

// test
initial begin
  
  // reset
  repeat(5) @(posedge clk_cpu);
  test_reset();
  
  // check program counter
  repeat(5) @(posedge clk_cpu);
  
  // mov rega 5
  @(negedge clk_cpu) begin
    cpu_proto_select_a <= 0;
     cpu_proto_select_b <= 0;
     cpu_proto_load_a <= 1;
     cpu_proto_load_b <= 0;
     cpu_proto_load_pc <= 0;
     cpu_proto_im <= 4'd5;
  end
  
  // mov regb rega
  @(negedge clk_cpu) begin
    cpu_proto_select_a <= 1;
     cpu_proto_select_b <= 0;
     cpu_proto_load_a <= 0;
     cpu_proto_load_b <= 1;
     cpu_proto_load_pc <= 0;
     cpu_proto_im <= 4'd0;
  end
  
  // add regb 3
  @(negedge clk_cpu) begin
    cpu_proto_select_a <= 0;
     cpu_proto_select_b <= 1;
     cpu_proto_load_a <= 0;
     cpu_proto_load_b <= 1;
     cpu_proto_load_pc <= 0;
     cpu_proto_im <= 4'd3;
  end
  
  // jump 2
  @(negedge clk_cpu) begin
    cpu_proto_select_a <= 0;
     cpu_proto_select_b <= 0;
     cpu_proto_load_a <= 0;
     cpu_proto_load_b <= 0;
     cpu_proto_load_pc <= 1;
     cpu_proto_im <= 4'd2;
  end
  
  // reset
  repeat(5) @(posedge clk_cpu);
  test_reset();
end

// tasks
task test_reset; 
begin
  reset = 1;
  repeat(5) @(posedge clk_cpu)
    if (cpu_proto_pc != 4'd0)
      $display("test_reset_register_file: dat_out is not zero");
  reset = 0;
end
endtask

endmodule

ここではmov rega 5, mov regb rega, add regb 3, jump 2と実行しています。 f:id:steavevaivai:20190819153926p:plain

実際にシミュレーションしみるとreg_a_out, reg_b_out, cpu_proto_pcの値からその通りになっていることが確認できます。

ROMから読み込んだ命令を実行する

直前のシミュレーションでは命令を実行するためにloadやselectの値を直接渡していましたが、実際はメモリから命令を読み込んで実行するはずなのでそうできるようにしたいと思います。読み込みのみ可能なROMを以下のように実装します。 Rom_Proto.v

module Rom_Proto (
  input   clk_cpu,
  input   reset,
  input   [3:0] adrs,
  output  [8:0] dat_out
);

  // ROM instruction data
  function [7:0] rom_data (
    input [3:0] rom_adrs
  );
    begin
      case (rom_adrs)
        4'h0: rom_data = {4'd5, 5'b00100}; // mov rega 5
        4'h1: rom_data = {4'd0, 5'b01001}; // mov regb rega
        4'h2: rom_data = {4'd3, 5'b01010}; // add regb 3
        4'h3: rom_data = {4'd0, 5'b10000}; // jump 0
        4'h4: rom_data = {4'd0, 5'b00000};
        4'h5: rom_data = {4'd0, 5'b00000};
        4'h6: rom_data = {4'd0, 5'b00000};
        4'h7: rom_data = {4'd0, 5'b00000};
        4'h8: rom_data = {4'd0, 5'b00000};
        4'h9: rom_data = {4'd0, 5'b00000};
        4'ha: rom_data = {4'd0, 5'b00000};
        4'hb: rom_data = {4'd0, 5'b00000};
        4'hc: rom_data = {4'd0, 5'b00000};
        4'hd: rom_data = {4'd0, 5'b00000};
        4'he: rom_data = {4'd0, 5'b00000};
        4'hf: rom_data = {4'd0, 5'b00000};
        default: rom_data = 9'h0;
      endcase
    end
  endfunction

  assign dat_out = rom_data(adrs);

endmodule

データのフォーマットを決めて置きプログラムカウンタの値によってまとめて取り出すようにしています。今回のデータのフォーマットは {im, load_pc, load_b, load_a, select_b, select_a}のようになっています。

データのフォーマットに注意してシミュレーションの実装は以下のようになりました。assignで正しいbitを渡すようにしています。

`include "Defines.v"

module CPU_ROM_ProtoTest();
  
reg clk;
reg reset;
wire select_a;
wire select_b;
wire load_a;
wire load_b;
wire load_pc;
wire [3:0] im;
wire [3:0] pc;
wire [8:0] inst;

Rom_Proto Rom_Proto0(
  .clk_cpu(clk), 
  .reset(reset), 
  .adrs(pc), 
  .dat_out(inst)
);

assign select_a = inst[0];
assign select_b = inst[1];
assign load_a = inst[2];
assign load_b = inst[3];
assign load_pc = inst[4];
assign im = inst[8:5];

CPU_Proto CPU_Proto0 (
  .clk_cpu(clk),
  .reset(reset),
  .select_a(select_a),
  .select_b(select_b),
  .load_a(load_a),
  .load_b(load_b),
  .load_pc(load_pc),
  .im(im),
  .pc(pc)
);

// clock
initial
begin
    clk = 0;
    forever #`HCYCL clk = !clk;
end

initial begin
  @(negedge clk) begin
    reset = 1;
    repeat(5) @(posedge clk)
    reset = 0;
  end
end

endmodule

これのシミュレーションを実行しようとするとターミナルにエラーが出るのですがシミュレーションのウィンドウからLibrary -> work -> CPU_ROM_ProtoTestを右クリックしてsimulateを選択するとシミュレーションが実行できます。結果は以下のようになり、想定通りになりました。 f:id:steavevaivai:20190819154000p:plain

命令のデコーダを作成する

上記のROMだとselect, loadの値をそのまま保持しているのですが、selectやloadの値をどうするかcpu内部で判断できるようにデコーダを作成してみます。

  • Decoder.v
`include "Defines.v"

module Decoder (
  input   [3:0] op_in,
  input   alu_carry,
  output  [1:0] alu_data_sel,
  output  reg_a_load,
  output  reg_b_load,
  output  reg_pc_load
);

  // decode operation and generate control path signals
  // format = {data selector for ALU, pc_load, b_load, a_load}
  function [4:0] decode_op;
    input [3:0] op;
    input alu_carry_in;
    case (op)
      `OP_NOP:      decode_op = {`SEL_Z, 1'b0, 1'b0, 1'b0};
      `OP_ADD_A_IM: decode_op = {`SEL_A, 1'b0, 1'b0, 1'b1};
      `OP_ADD_B_IM: decode_op = {`SEL_B, 1'b0, 1'b1, 1'b0};
      `OP_MOV_A_IM: decode_op = {`SEL_Z, 1'b0, 1'b0, 1'b1};
      `OP_MOV_B_IM: decode_op = {`SEL_Z, 1'b0, 1'b1, 1'b0};
      `OP_MOV_A_B:  decode_op = {`SEL_A, 1'b0, 1'b0, 1'b1};
      `OP_MOV_B_A:  decode_op = {`SEL_B, 1'b0, 1'b1, 1'b0};
      `OP_JMP_IM:   decode_op = {`SEL_Z, 1'b1, 1'b0, 1'b0};
      `OP_JNC_IM:   
        if (~alu_carry_in)
                    decode_op = {`SEL_Z, 1'b1, 1'b0, 1'b0};
        else
                    decode_op = {`SEL_Z, 1'b0, 1'b0, 1'b0};
      default:      decode_op = {`SEL_Z, 1'b0, 1'b0, 1'b0};
    endcase
  endfunction

  // decompose decoded bits into control paths
  reg [5:0] decoded;
  always @ (op_in or alu_carry) begin
    decoded = decode_op(op_in, alu_carry);
  end
  assign reg_a_load = decoded[0];
  assign reg_b_load = decoded[1];
  assign reg_pc_load = decoded[2];
  assign alu_data_sel = decoded[4:3];
 
endmodule
  • Defines.v
`timescale 1 ns/ 1 ns

`define HCYCL 10
`define HSTRB 8

// data select for ALU
`define SEL_A   2'b00     // data select register A
`define SEL_B   2'b01     // data select register B
`define SEL_Z   2'b11     // data select ZERO

// operations
`define OP_NOP        4'h0
`define OP_ADD_A_IM   4'h1
`define OP_ADD_B_IM   4'h2
`define OP_MOV_A_IM   4'h3
`define OP_MOV_B_IM   4'h4
`define OP_MOV_A_B    4'h5
`define OP_MOV_B_A    4'h6
`define OP_JMP_IM     4'h7
`define OP_JNC_IM     4'h8

デコーダへの入力op_inが命令になっており、この値によりload, selectの値が変わるようになっています。またOP_JNC_IMは条件付きのジャンプ命令になっていてalu_carry_inが立っていない場合のみジャンプするというものになるのですが、こういった条件付きの命令を作ることを考えてもデコーダはcpuの内部にあったほうが良いのかと思います。

今回のデコーダに合わせてcpu, romの実装を以下のように修正します。

  • Rom.v
`include "Defines.v"

module Rom (
  input   clk_cpu,
  input   reset,
  input   [3:0] adrs,
  output  [7:0] dat_out
);

  // ROM instruction data
  function [7:0] rom_data (
    input [3:0] rom_adrs
  );
    begin
      case (rom_adrs)
        4'h0: rom_data = {`OP_ADD_A_IM, 4'h1};
        4'h1: rom_data = {`OP_JNC_IM,   4'h0};
        4'h2: rom_data = {`OP_ADD_A_IM, 4'h1};
        4'h3: rom_data = {`OP_JNC_IM,   4'h2};
        4'h4: rom_data = {`OP_ADD_A_IM, 4'h1};
        4'h5: rom_data = {`OP_JNC_IM,   4'h4};
        4'h6: rom_data = {`OP_ADD_A_IM, 4'h1};
        4'h7: rom_data = {`OP_JNC_IM,   4'h6};
        4'h8: rom_data = {`OP_ADD_A_IM, 4'h1};
        4'h9: rom_data = {`OP_JNC_IM,   4'h8};
        4'ha: rom_data = {`OP_JMP_IM,   4'ha}; // 自分自身にジャンプで処理終了
        4'hb: rom_data = {`OP_NOP,      4'h0};
        4'hc: rom_data = {`OP_NOP,      4'h0};
        4'hd: rom_data = {`OP_NOP,      4'h0};
        4'he: rom_data = {`OP_NOP,      4'h0};
        4'hf: rom_data = {`OP_NOP,      4'h0};
        default: rom_data = 8'h0;
      endcase
    end
  endfunction

  assign dat_out = rom_data(adrs);

endmodule
  • CPU_Decoder.v
`include "Defines.v"

module CPU_Decoder (
  input clk_cpu,
  input reset,
  input [7:0] inst,
  output [3:0] pc
);
  
  reg [3:0] next_pc;
  wire load_a;
  wire load_b;
  wire load_pc;
  wire [3:0] reg_a_out;
  wire [3:0] reg_b_out;
  wire [3:0] alu_data_0_in;
  wire [3:0] alu_data_1_in;
  wire [3:0] alu_data_out;
  wire [1:0] alu_data_sel;
  wire alu_carry_out;

  // program counter
  RegisterFile RegisterPC(
    .clk_cpu(clk_cpu), 
    .reset(reset), 
    .load(clk_cpu), 
    .dat_in(next_pc), 
    .dat_out(pc)
  );
  always @(*) begin
    if (load_pc)
      next_pc <= alu_data_out;
    else
      next_pc <= pc + 4'd1;
  end
  
  // Decoder
  Decoder Decoder0 (
    .op_in(inst[7:4]),
     .alu_carry(alu_carry_out),
     .alu_data_sel(alu_data_sel),
     .reg_a_load(load_a),
    .reg_b_load(load_b),
    .reg_pc_load(load_pc)
  );
  
  // register A
  
  RegisterFile RegisterFileA(
    .clk_cpu(clk_cpu), 
    .reset(reset), 
    .load(load_a), 
    .dat_in(alu_data_out), 
    .dat_out(reg_a_out)
  );
  
  // register B
  RegisterFile RegisterFileB(
    .clk_cpu(clk_cpu), 
    .reset(reset), 
    .load(load_b), 
    .dat_in(alu_data_out), 
    .dat_out(reg_b_out)
  );
  
  // ALU
  assign alu_data_0_in =
    alu_data_sel == `SEL_A ? reg_a_out :
    alu_data_sel == `SEL_B ? reg_b_out :   
    alu_data_sel == `SEL_Z ? 4'd0 : 
    4'd0;
  assign alu_data_1_in = inst[3:0];
  Alu Alu0(
    .clk(clk_cpu),
    .reset(reset),
    .data_0_in(alu_data_0_in),
    .data_1_in(alu_data_1_in),
    .data_out(alu_data_out),
    .carry_out(alu_carry_out)
  );
   
endmodule

それから動作検証用のモジュールを追加して想定通り動作していることを確認します。

  • CPU_DecoderTest.v
`include "Defines.v"

module CPU_DecoderTest();
  
reg clk;
reg reset;
wire [7:0] inst;
wire [3:0] pc;

Rom Rom0(
  .clk_cpu(clk), 
  .reset(reset), 
  .adrs(pc), 
  .dat_out(inst)
);

CPU_Decoder CPU_Decoder0(
  .clk_cpu(clk),
  .reset(reset),
  .inst(inst),
  .pc(pc)
);

// clock
initial
begin
    clk = 0;
    forever #`HCYCL clk = !clk;
end

initial begin
  @(negedge clk) begin
    reset = 1;
    repeat(5) @(posedge clk)
    reset = 0;
  end
end

endmodule

f:id:steavevaivai:20190819154020p:plain

今回は "レジスタAの値を1追加" -> "alu_carry_outが0なら前の命令に戻る" というのを5回繰り返しています。

クロック周波数を調整してキッチンタイマーとして使えるようにする

"レジスタAの値を1追加" -> "alu_carry_outが0なら前の命令に戻る"を5回繰り返すとしてalu_carry_outが1になるタイミングは16回実行語になるので命令を実行する回数は全部で2 * 16 * 5 = 160回になります。ただ現在10ns毎でクロックが反転するようになっており命令を実行するのはクロックの立ち上がりのタイミングになっているので一命令の実行に20nsかかるので処理の完了までに160 * 20 = 3200nsとなり一瞬で終了するのでキッチンタイマーとして使えません。 そこで以下のモジュールでクロックの周波数を変更できるようにします。

  • ClockPrescaler.v
`include "Defines.v"

module ClockPrescaler (
  input   clk,
  input   reset,
  output  reg clk_cpu
);

  reg [31:0] cnt = 32'd0;
  
  always @(posedge clk or posedge reset) begin
    if (reset) begin
      cnt <= 32'd0;
    end else if (cnt == `CPU_CLK_PRESCALE) begin
      cnt <= 32'd0;
    end else begin
      cnt <= cnt + 32'd1;
    end
  end

  // clk_cpu register (clock for cpu)
  always @(posedge clk or posedge reset) begin
    if (reset)
      clk_cpu <= 0;
    else
      clk_cpu <= cnt < (`CPU_CLK_PRESCALE / 2);
  end
  
endmodule
  • Defines.v
`timescale 1 ns/ 1 ns

`define HCYCL 10
`define HSTRB 8

// CPU prescaling (1 cycle per sec)
//`define CPU_CLK_PRESCALE 32'd49999999
`define CPU_CLK_PRESCALE 32'd3

// data select for ALU
`define SEL_A   2'b00     // data select register A
`define SEL_B   2'b01     // data select register B
`define SEL_Z   2'b11     // data select ZERO

// operations
`define OP_NOP        4'h0
`define OP_ADD_A_IM   4'h1
`define OP_ADD_B_IM   4'h2
`define OP_MOV_A_IM   4'h3
`define OP_MOV_B_IM   4'h4
`define OP_MOV_A_B    4'h5
`define OP_MOV_B_A    4'h6
`define OP_JMP_IM     4'h7
`define OP_JNC_IM     4'h8

動作検証用のモジュールで変換したクロックを渡すようにします。 - CPU_ClockPrescalerTest

`include "Defines.v"

module CPU_ClockPrescalerTest();
  
reg clk;
reg reset;
wire clk_cpu;
wire [7:0] inst;
wire [3:0] pc;

// cpu clock prescaling
ClockPrescaler ClockPrescaler0(
  .clk(clk), 
  .reset(reset), 
  .clk_cpu(clk_cpu)
);
  
Rom Rom0(
  .clk_cpu(clk_cpu), 
  .reset(reset), 
  .adrs(pc), 
  .dat_out(inst)
);

CPU_Decoder CPU_Decoder0(
  .clk_cpu(clk_cpu),
  .reset(reset),
  .inst(inst),
  .pc(pc)
);

// clock
initial
begin
    clk = 0;
    forever #`HCYCL clk = !clk;
end

initial begin
  @(negedge clk) begin
    reset = 1;
    repeat(5) @(posedge clk)
    reset = 0;
  end
end

endmodule