Hatena::ブログ(Diary)

言語ゲーム このページをアンテナに追加 RSSフィード Twitter

とあるエンジニアが嘘ばかり書く日記
ホームページ | blog | twitter

2008-06-16

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

FlorianFlorian 2008/06/16 14:34 おおおおお!
すごい!おもしろい!


ときに「CPU は計算器というよりはデータ転送器械であるというのが著者の主張」って、原著のどこにありますか? 昔、授業のために嘘命令セットをでっち上げたときには、「MOV命令は計算を行わない演算命令の一種」という説明をしたので、非常にスタンスが似てるなぁ、と思いまして。この本、読んだはずなんだけどなぁ……。
ちなみにその授業で説明用に作ったのは、「全命令が共通のコンディション判断ビットを持つ、演算命令しかないCISCの命令セット」でした。

propellapropella 2008/06/16 15:15 いい本ですよね。これ。えーと。上手くピッタリ引用できないのですが、71ページの

演算はソフトウェアから見ると転送命令とは別のカテゴリですが、CPU の回路としては MOV などの転送命令 (として処理できる) の一種です。データを転送する途中で演算回路 (上の例だとインバーター)を経由すればよい、ということです。演算命令と転送命令はほとんど同じ。これも非常に重要。

のあたりがそういう主張かなと思いました。

FlorianFlorian 2008/06/16 17:42 ありがとうございます。わぁ、ぴったり一緒だぁ!>主張
当時なぜここに気づかなかったんだろう?

ちなみに私がこれに気づいたのはZ80の命令セット表を3bitごとにまとめて眺めていたときでした。実に小学6年生。マシン語少年の魂は不滅ですなぁ(^^;)。

propellapropella 2008/06/17 02:07 Z80 の命令を 3 bitごとにまとめると何が出てくるんですか?

squeakersqueaker 2008/06/18 14:47 MIPSでもmov命令は存在しなくてr0との加算でやっていました。

suzumura_sssuzumura_ss 2009/12/15 20:49 > Spartan-3A
あなたは私かと(笑
参考にさせて頂きます。「CPUの〜」は名著ですね。

Z80を3bitにまとめると、B/C/D/E/H/L/(HL)/A なる8種類のsrc/dstが出てきますね。

のふのふ 2010/10/17 23:45 このたび弊サイトにてダシにさせていただきましたので
http://nk01ke.cocolog-nifty.com/blog/2010/10/cpu-b740.html
ご報告まで。
ご迷惑でしたら取り下げますです。


カレンダー
<< 2008/06 >>
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
最新コメント一覧