2011年11月24日木曜日

独自のCPUを創ってみる


by yosikawa » 2011年11月13日(日) 11:05
さて、TD4を実装してみてCPUの作り方に慣れてきたので俺様CPUを創ってみたい。

当面の目標

自分でCPUを設計して実装する技量を身につけたい。
Spartan starter kit に搭載されているDRAMを使いたい。
wishbone の使い方を学びたい。


CPUを作りたい理由は、FPGAの技能を高めたいから。
そのためには実戦経験が必要。でもつまらないテーマではヤル気が出ないし、
いきなり難しすぎると挫折は目に見えている。その点、CPUはお手頃のように思える。

DRAMを使いこなしたいのは、FPGAで何かまともなものを作るには
大容量のメモリが欠かせないから。
ディスプレイにグラフィックを表示するだけでも、DRAMが必要になる。
以前作った固定のパターンならメモリ不要で簡単だったが、これでは動きもないし、まったくつまらない。

wishboneについては、もともと目標ではなかった。
しかし自分でSDRAMコントローラを作るのはまだ無理なので、
SDRAMコントローラをネットで拾ってきたところ、そのインタフェースがwishbone だった。
そこでこの際、wishboneを使いこなしたい。


最初の一歩

実は最初にあれこれ試した。
CPUアーキテクチャの図をよく目にするので、自分なりに書いてみたり、
以前書いたTD4をベースにとにかくごりごり書いてみたり。

でも収集がつかなくなり、失敗。
改めて冷静に考えなおした。

ごくシンプルに設計。。。しよう。
アキュムレータとMV命令だけ作ってみる。


命令はこんな感じ。

コード: 
MV A,im


Aは(ひとつだけの)レジスタ。
imはイミディエイトデータ。(この命令の直後のアドレスのメモリの値のこと。命令の引数のようなもの)

レジスタの変化に着目することにした。
以下のような考え方は自分独自の我流なので、
一般的ではないのかも知れないが、ともかくこれで最初の一歩はうまく行った。

まずはプログラムカウンタ。これがなければ始まらないだろう。

pc_c: next pc
0: pc
1: pc+1

pc_c はpcの変化を制御する信号。
next pc とは、pcの次の値。(変化した後の値。フリップフロップの入力値とも言える)
pc_c==0 のとき、pcは値を保持する。
pc_c==1 のとき、pcは次のアドレスを指す。

普通、CPUは動きを止めずにどんどん進むので pc_c==0 は考えなくてもいいのだが、
ここでは実験用ということもあるし、wishboneのハンドシェークによって
メモリの応答を待っている間も動作を止める必要があるのでこのように考えた。
将来JMP命令を作るときに pc_c==2 のときジャンプ先を指すようにするなど、
さらに複雑化する予定で、今のところはとりあえず2種類の状態遷移を考えた。

さて、pc_c 信号を作らなければならない。
opencoreのwishboneの仕様書を読んだところ、
一番単純なSINGLE READサイクルは以下のようになっている。

SEL_OとTGX_Xは使わないので無視。
要は CYC_O=1, STB_O=1, WE_O=0 を出力するとメモリなどの周辺回路から読み込みできる。読み込み先のアドレスはADR_Oに出力する。
周辺回路が準備OKのときACK_I=1 になるので、このタイミングでDAT_Iから読みこめばよい。
従って単純に pc_c = ACK_I で良い。

以上をverilogで書くと、以下のようになった。
コード: 
   reg [31:0] pc;
   wire pc_c = ACK_I;
   always @(posedge clk) begin
      if(rst) begin
         pc <= 0;
      end else if(pc_c) begin
         pc <= pc + 1;
      end
   end
   assign ADR_O = pc;

clkはwishboneのクロック入力CLK_I、
rstは同じくwishboneのリセット入力RST_Iである。
リセットは同期リセットと決められている。

wishboneの制御信号は以下のようにして作る。
コード: 
   reg stb = 0;
   assign CYC_O = stb;
   assign STB_O = stb;
   always @(posedge clk) begin
      if(rst) begin
         stb <= 0;
      end else if(stb & ack) begin
         stb <= 0;
      end else if(cpu_en) begin
         stb <= 1;
      end
   end

cpu_en はwishbone規格外の入力信号で、1のときCPUが進み、0にするといつでもCPUを停止できる。
cpu_en==1 のときstb=1に変化しメモリからの読み込みが始まる。
ack==1のときに読み込みが完了して stb=0 となる。
ack信号がstbよりも遅れるとは限らない。十分速く応答できるデバイスなら、
そのデバイスの入力信号であるSTB_I (STB_Oと同じ) をそのままACK_O に出力してよく、
このACK_OがCPUの入力ACK_I になるので、stbとackが同じになりうる。

次に考えたのはCPUサイクルを表すレジスタ cpucyc。
今サポートしている命令はただひとつ、MV A,im だけである。
メモリは例えば以下のようになっている。

pc+0: 0 (MV A,im の命令コード)
pc+1: 1234 (imの値)
pc+2: 0 (次の命令)

CPUサイクルは命令フェッチとイミディエイトデータ読み込みの2種類がある。
先ず、pc+0のアドレスから命令コードをフェッチする。
次に、pcが+1されてpc+1のアドレスからイミディエイトデータを読み込む。
以後、繰り返しとなる。

そこで以下のように定義する。
コード: 
// CPUサイクル
`define FETCH 0      // 命令フェッチ
`define IMM 1      // イミディエイトデータ読み込み


cpucycの状態遷移は以下のようになる。

cpucyc: next cpucyc
FETCH: IMM
IMM: FETCH

verilogコードは以下のようになった。
コード: 
   reg [3:0] cpucyc;
   always @(posedge clk) begin
      if(rst) begin
         cpucyc <= `FETCH;
      end else if(ack) begin
         case (cpucyc)
            `FETCH: cpucyc <= `IMM;
            `IMM: cpucyc <= `FETCH;
         endcase
      end
   end
   assign WE_O = 0;


最後にレジスタの状態遷移を考える。
考え方はこれまでと同じ。
レジスタacca (アキュムレータAの意味)を定義し、状態遷移を制御する信号acca_cに応じて変化させる。

acca_c: next acca
0: acca
1: DAT_I

レジスタの入力値は、今はイミディエイトのみなので常にwishbone入力データバス DAT_I である。
acca_c はACK_I==1 でかつ、CPUサイクルがIMMのとき1になるので、コードは以下のようになった。
コード: 
   // レジスタ
   reg [31:0] acca;
   wire acca_c = ACK_I & (cpucyc == `IMM);
   always @(posedge clk) begin
      if(rst) begin
         acca <= 0;
      end else if(acca_c) begin
         acca <= DAT_I;
      end
   end


これで完成。
CPUだけでは動作を確認できないので、簡単なROMと、
TD4のコードから取ってきた周辺回路を加えて添付のv1のコードができた。

以下は、シミュレーションしているところ。

カーソルが指しているのはACK_I=1となってメモリを読み込んだところ。
このときADR_O=pc=0だったので0番地のメモリの値 0をフェッチした。
ADR_Oは次の読み込みのため1番地を指し、DAT_Iからは次の番地の値 1234H が出力されている。
ここで実装したROMは非同期で、すぐにメモリの値が出力されるので STB_O と ACK_I が常に同じになっている。


発展

この調子で v1=> v2 => v3 => v4 と順に発展させていった。

v2 ではNOPをサポートした。
これで命令は2種類になり、命令コードをデコードする必要が出てきた。

wishbone 制御信号も改善した。
コード: 
   reg stb = 0;
   assign CYC_O = stb;
   assign STB_O = stb;
   always @(posedge clk) begin
      if(rst) begin
         stb <= 0;
      end else if(stb & ack) begin
         stb <= cpu_en;
      end else if(cpu_en) begin
         stb <= 1;
      end
   end

従来との違いは if(stb & ack) のとき、stb<=0 とする代わりに
stb<=cpu_en とすることで、休みなくすぐに次のメモリサイクルを開始するようにしたこと。
特に、メモリが高速で常にstb==ackとなるようにすぐにackを返してきて、
しかもcpu_enが常に1であれば、上記の回路は結局常に stb==1 かつ 常に ack==1 となり、
無駄なくクロックごとにcpuサイクルが進む。
しかもちゃんとハンドシェークしているので、もしメモリが遅ければackの応答を待って動作することもできる。

v3 ではJMPをサポートした。
pc レジスタの値の変化は以下のように3種類になった。

pc_c: next pc
0: pc
1: pc+1
2: DAT_I

プログラムは以下のようになった。(jmpはジャンプ命令実行中、
cycimはイミディエイトデータ読み込み中、ackはACK_I、indataはDAT_Iのこと)
コード: 
   // PC
   reg [31:0] pc;
   wire [1:0] pc_c = 
      (jmp & cycim & ack) ? 2'b10 :
      ack ? 2'b01 :
      2'b00;
   always @(posedge clk) begin
      if(rst) begin
         pc <= 0;
      end else begin
         case (pc_c)
            2'b00: pc <= pc;
            2'b01: pc <= pc + 1;
            2'b10: pc <= indata;
            default: pc <= pc;
         endcase
      end
   end
   assign addr = pc;


v4では、これまでの演習で慣れてきたので欲張ってインデックスレジスタを用いた
間接アドレッシングをサポートした。命令は以下のとおり。
最初の3つはサポート済み、残り5つが新たに追加した命令。
左の数字は命令コード。

0: NOP  ノーオペレーション。何もしない。
1: MV A,im  イミディエイトデータimをレジスタAに読み込む
2: JMP im  アドレスimに無条件ジャンプする
3: MV A,[im]  アドレスimのメモリをレジスタAに読み込む
4: MV [im],A  レジスタAの値をアドレスimに書きこむ
5: MV IX,im  イミディエイトデータimをインデックスレジスタIXに読み込む
6: MV A,[IX]  インデックスレジスタが指すアドレスIXのメモリをレジスタAに読み込む
7: MV [IX],A  レジスタAの値をアドレスIXに書きこむ

さて、ここまで命令コードを単純に作った順に0, 1, 2 と割り当てたので
命令のデコードと各種制御信号を作るのが複雑になってきた。
(それに、32bitもあるのに実質3bitしか使ってないし)

ここまで来たらあとは分岐命令と算術演算をサポートすれば
一応はいろんなプログラムを作れるようになるはず。。もう少しだ。
でもデコードがぐちゃぐちゃになってきたので、どこかで一度命令体系を整理しないと。

なお、32bitにしたのは、Spartan starter kitのDRAMが16bitで、
DDRでは1クロックで2回読むので32bit ずつアクセスすることになるから、
これに合わせるため。今回作ったCPUの規模なら8bitにした方が簡単だった。

0 件のコメント:

コメントを投稿