言語ゲーム

とあるエンジニアが嘘ばかり書く日記

Twitter: @propella

CPUの創りかた TD4 を Spartan-3A で

FPGA に慣れるために何が良いかなーと考えて、簡単そうなので『CPUの創りかた』の 4 ビット CPU TD4 を作ってみる事にした。インチキ臭いけど、まあいいか。CPU ってこんな風になっているのか!とわかって大変勉強になりました。大変素晴らしい本です。

module td4(
           input CLOCK,
           input RESET,
           input [3:0] IN,
           output reg [3:0] OUT
           );

   reg [3:0] A, B; // Registers
   reg [3:0] PC; // Program Counter
   reg [7:0] ROM [15:0]; //the Instruction memory
   reg C; // Carry flag

   wire Cnext;
   wire [3:0] OP; // Operation code
   wire [3:0] IM; // Immediate data
   wire [3:0] CHANNEL; // Input channel
   wire [3:0] ALU;
   wire LOAD0, LOAD1, LOAD2, LOAD3;
   wire SELECT_A, SELECT_B;

外に出ている信号はクロック、リセット、入出力で、ROM は FPGA の中に作っています。レジスタは A と B の二つとプログラムカウンタだけ。そのほかの信号の名前は本にあわせました。

// 0000 ADD A, Im
// 0001 MOV A, B
// 0010 IN  A
// 0011 MOV A, Im

// 0100 MOV B, A
// 0101 ADD B, Im
// 0110 IN  B
// 0111 MOV B, Im

// 1001 OUT B
// 1011 OUT Im
// 1110 JNC Im (Jump if No Carry)
// 1111 JMP Im

   assign IM = ROM[PC][3:0];
   assign OP = ROM[PC][7:4];

機械語は、上位四ビットが命令で、下位四ビットがデータです。データを使わない時はゼロを入れる約束です。

   // Data transfar
   always @(posedge CLOCK) begin
      C   <= RESET ? 0 :  Cnext;
      A   <= RESET ? 0 : ~LOAD0 ? ALU : A;
      B   <= RESET ? 0 : ~LOAD1 ? ALU : B;
      OUT <= RESET ? 0 : ~LOAD2 ? ALU : OUT;
      PC  <= RESET ? 0 : ~LOAD3 ? ALU : PC + 1;
   end

ここが味噌の部分。CPU は計算器というよりはデータ転送器械であるというのが著者の主張なので、それを表現してみたつもり、クロックと条件に合わせてデータを転送します。LOAD0 〜 LOAD3 は、本に合わせて負論理です。

   // Opcode decode
   assign SELECT_A = OP[0] | OP[3];
   assign SELECT_B = OP[0];
   assign LOAD0 =  OP[2] |  OP[3];
   assign LOAD1 = ~OP[2] |  OP[3];
   assign LOAD2 =  OP[2] | ~OP[3];
   assign LOAD3 = ~OP[2] | ~OP[3] | (~OP[0] & C);

命令デコーダは IC で作ると大変そうですが、Verilog ならあっさりしています。

   // Data selector
   assign CHANNEL = (~SELECT_B & ~SELECT_A) ? A :
                    (~SELECT_B &  SELECT_A) ? B :
                    ( SELECT_B & ~SELECT_A) ? IN :
                    4'b0000;

   // ALU
   assign {Cnext, ALU} = CHANNEL + IM;

ここも本に合わせて、入力データを選択する部分と、加算器の部分です。回路を単純にするために、使わなくても加算されるというのが面白い。これが邪魔にならないよう、入力データとして必ず 0 が入るモードが用意されています。

   // Ramen timer
   initial begin
      ROM[0] =  8'b10110111; // OUT 0111   # LED
      ROM[1] =  8'b00000001; // ADD A,0001
      ROM[2] =  8'b11100001; // JNC 0001   # loop 16 times
      ROM[3] =  8'b00000001; // ADD A,0001
      ROM[4] =  8'b11100011; // JNC 0011   # loop 16 times
      ROM[5] =  8'b10110110; // OUT 0110   # LED
      ROM[6] =  8'b00000001; // ADD A,0001
      ROM[7] =  8'b11100110; // JNC 0110   # loop 16 times
      ROM[8] =  8'b00000001; // ADD A,0001 
      ROM[9] =  8'b11101000; // JNC 1000   # loop 16 times
      ROM[10] = 8'b10110000; // OUT 0000   # LED
      ROM[11] = 8'b10110100; // OUT 0100   # LED
      ROM[12] = 8'b00000001; // AND 0001
      ROM[13] = 8'b11101010; // JNC 1010   # loop 16 times
      ROM[14] = 8'b10111000; // OUT 1000   # LED
      ROM[15] = 8'b11111111; // JMP 1111
   end

endmodule

ラーメンタイマーです。ここまででシミュレーションする事が出来ますが、Spartan のクロックは 50MHz なので、クロックを遅くする部品を作りました。

`define PAR_CLOCK 50000000

module top_td4(
            input CLOCK,
            input RESET,
            input [3:0] IN,
            output [3:0] OUT
            );

   reg [26:0] counter;
   wire td4_CLOCK;
   assign td4_CLOCK = counter < `PAR_CLOCK / 2;

   td4 td4_0(td4_CLOCK, RESET, IN, OUT);
   
   always @(posedge CLOCK) begin
      counter <= RESET | counter < `PAR_CLOCK ? counter + 1 : 0;
   end
   
endmodule

あとは制約ファイルを作って完成。

NET "CLOCK" LOC = "E12"| IOSTANDARD = LVCMOS33 ;

NET "IN<0>" LOC = "V8" | IOSTANDARD = LVTTL | PULLUP ;
NET "IN<1>" LOC = "U10"| IOSTANDARD = LVTTL | PULLUP ;
NET "IN<2>" LOC = "U8" | IOSTANDARD = LVTTL | PULLUP ;
NET "IN<3>" LOC = "T9" | IOSTANDARD = LVTTL | PULLUP ;

NET "OUT<3>" LOC = "U19" | IOSTANDARD = LVTTL | SLEW = QUIETIO | DRIVE = 4 ;
NET "OUT<2>" LOC = "U20" | IOSTANDARD = LVTTL | SLEW = QUIETIO | DRIVE = 4 ;
NET "OUT<1>" LOC = "T19" | IOSTANDARD = LVTTL | SLEW = QUIETIO | DRIVE = 4 ;
NET "OUT<0>" LOC = "R20" | IOSTANDARD = LVTTL | SLEW = QUIETIO | DRIVE = 4 ;

NET "RESET" LOC = "T15" | IOSTANDARD = LVTTL | PULLDOWN ; # SOUTH BUTTON

本当はブロックRAM という物を使ってみたかったのだけど、今日はそこまで行き着きませんでした。また今度チャレンジします。