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 という物を使ってみたかったのだけど、今日はそこまで行き着きませんでした。また今度チャレンジします。