2011年11月24日木曜日

独自のCPUを創ってみる(3)


独自のCPUを創ってみる(2)の続き


だんだん本格的なCPUみたいになってきたので、がぜんヤル気が出てきた。

命令セットの拡張

余っている instr[ 7: 0] をイミディエイトデータに活用する。
8bit の2の補数を32bitに符号拡張して何かに加算する。
フラグを更新する/しないを決めるために1bit割り当てる。

// 命令コード体系
wire [3:0] opalu  = instr[31:28]; // 演算の種別 (4)
wire       opflag = instr[27]; // フラグに反映する/しない (1)
wire [2:0] opaddr = instr[26:24]; // アドレッシングモード (3)
wire [3:0] opcond = instr[23:20]; // 条件付き実行フラグ (4)
wire [3:0] opregd = instr[19:16]; // 結果を格納するレジスタRdの指定 (4)
wire [3:0] opregi = instr[15:12]; // オペランドレジスタRiの指定 (4)
wire [3:0] opregj = instr[11: 8]; // オペランドレジスタRjの指定 (4)
wire [7:0] opim8  = instr[7:0]; // イミディエイトデータ im8 (8)
wire [31:0] im8 = { {24{opim8[7]}}, opim8 }; // 符号拡張

自動インクリメント/デクリメント付き命令を追加する。
32bit イミディエイトデータ読み込みは廃止する。

// アドレッシングモード
// 注: Riを使わない演算のときopxxiはRiをインクリメントしない
wire oprr = (opaddr == 0); // Rd = Ri op (Rj+im8)
wire opra = (opaddr == 1); // Rd = Ri op [im32]
wire oprx = (opaddr == 2); // Rd = Ri op [Rj+im8*4]
wire oprxi = (opaddr == 3); // Rd = Ri op [Rj+im8*4]+
wire opxxi = (opaddr == 4); // [Rd]+ = [Ri]+ op [Rj+im8*4]+
wire opar = (opaddr == 5); // [im32] = Ri op (Rj+im8)
wire opxr = (opaddr == 6); // [Rd+im8*4] = Ri op Rj
wire opxrd = (opaddr == 7); // [Rd+im8*4]- = Ri op Rj

イミディエイトアドレッシングを廃止したので、レジスタに定数を設定するには、どこか他のアドレスに定数を置いて、そのアドレスをしていしてopraモードでアクセスする。

opxxという形式のネーミングは、3文字目が結果の格納先、つまり左辺を示し、4文字目は右辺の右オペランド、つまりRjを示している。文字rはレジスタ、a はアドレス参照、xはレジスタ間接アドレッシングを示している。例えばレジスタを参照して計算して結果をレジスタに格納するならoprrとなる。サフィックスのiは自動インクリメント、dは自動デクリメントを指している。

oprxi と opxrd を用いれば、任意のレジスタをスタックポインタとして利用できる。本来ならポストインクリメント、プレデクリメントとしたいところだが、実装が複雑になりそうなのでポストインクリメント、ポストデクリメントになってしまった。幸い、8bitイミディエイトデータ im8 が利用できるので、以下のようにしてpush/popができる。

push R0 : MV [R12-#4]-, R0
pop R0 : MV R0, [R12]+

R12をスタックポインタとして使う場合、[R12-#4]- は、R12が指すアドレスより4小さいアドレスを指しているので、実質的にプレデクリメントと同じアドレスに書きこむ。その後でR12がポストデクリメントによって4引かれる。

opxxi は、C言語のmemcpyのようなブロック転送を簡単に実現することを意図して追加した。
MV命令に対して使うと MV [Rd]+, [Ri+#nn]+ の形式の命令となり、Riが指すアドレスからRdが指すアドレスにデータをコピーし、かつRdとRiを自動でインクリメントする。

しかも、opxxi は1命令で読み込みと書込を実行するので、演算の内容をうまく工夫すればテスト&セット命令にも使えそうだ。

条件付き実行についても、少し工夫をしてみた。
R13 をカウンタ・レジスタとする。
条件フラグがCNZ (instr[23:20]==1)のとき、R13 != 0 の条件で
実行するとともに、R13をデクリメントする。
その条件付き実行と分岐を組み合わせると、回数が固定のループを1命令で実行できる。
先ほどのブロック転送用のopxxiと合わせて2命令でブロック転送ができる。

条件付き実行の条件は以下のようになった。

// 0: AL   無条件
// 1: CNZ  R[13] != 0, R[13]<=R[13]-1
// 2: CC   Ri >= Rj,C=0
// 3: CS   Ri <  Rj,C=1
// 4: NE   Ri != Rj,Z=0
// 5: EQ   Ri == Rj,Z=1
// 6: PL   Rd >= 0, N=0
// 7: MI   Rd <  0, N=1
// 8: VC   オーバフローでない,V=0
// 9: VS   オーバフロー,V=1
// 10:HI   Ri >  Rj (符号なし), C=0 AND Z=0
// 11:LS   Ri <= Rj (符号なし), C=1 OR Z=1
// 12:LT   Ri <  Rj, N!=V
// 13:GE   Ri >= Rj, N==V
// 14:GT   Ri >  Rj, Z=0 AND N==V
// 15:LE   Ri <= Rj, Z=1 AND N!=V

フラグレジスタは以下の5個ある。

reg fn; // 符号フラグ
reg fz; // ゼロフラグ
reg fc; // キャリーフラグ
reg fv; // オーバフローフラグ
reg fcnz; // カウンタ・ノンゼロ・フラグ

演算の種類は以下の通り。

// 0: MV   Rd = Rj            代入
// 1: LSR  Rd = 0,Rj>>1       右シフト (C=Rj[0])
// 2: ASR  Rd = Rj[31],Rj>>1  算術右シフト (C=Rj[0])
// 3: ROR  Rd = C,Rj>>1       右ローテート (C=Rj[0])
// 4: ADD  Rd = Ri + Rj       加算
// 5: ADC  Rd = Ri + Rj + C   キャリー付き加算
// 6: SUB  Rd = Ri - Rj       減算
// 7: SBC  Rd = Ri - Rj - C   キャリー付き減算
// 8: RSB  Rd = Rj - Ri       オペランド逆転減算
// 9: RSC  Rd = Rj - Ri - C   キャリー付き逆転減算
//10: MUL  Rd = Ri * Rj       乗算 (ALU_MUL==1 のとき)
//11: AND  Rd = Ri AND Rj     論理積
//12: OR   Rd = Ri OR  Rj     論理和
//13: XOR  Rd = Ri XOR Rj     排他的論理和
//14: CMP       Ri - Rj       比較
//15: TST       Ri AND Rj     テスト

汎用レジスタは以下の定義で16個ある。

reg [31:0] gr[0:15];

レジスタの使い方は以下のように定める。

R0~R11 : 汎用
R12 : スタックポインタ
R13 : カウンタレジスタ
R14 : リターンアドレス
R15 : プログラムカウンタ

R15以外は任意の目的に転用できる。
これらの仕様は今後も随時見直すだろう。


テストベンチ

設計の方が楽しいが、面倒なテストもやらねば使い物にならない。
以下のような簡単なコードで、テスト結果を確認できるようにしたい。

assert(PC == 11 && R0 == 32'h55);

assertは自分で定義したタスク。PC, R0は以下のように自分で定義した信号で、
テストベンチから簡単にアクセスできるように意図したものだ。

wire [7:0] PC = uut.td32.gr[15][9:2];
wire [31:0] R0 = uut.td32.gr[0];

上記の例では、プログラムカウンタの値が11で、かつR0の値が0x55であることを確認している。
最初にタイミング合わせをする必要がある。以下のタスクを定義した。

// PCが引数で指定された値になるまで待つ
task waitpc;
input [7:0] expected_pc;
for(i=0; i<255 & PC != expected_pc; i=i+1) begin
@(posedge fetch_start)
#1;
end
endtask

fetch_start は、cpuのフェッチサイクルの途中で一度だけパルスが立ち上がるように作った信号である。ループを回しながら、PCの値をチェックして、期待した値になるまで待つことでタイミングを合わせる。

次にassertの中で使うため、レジスタの内容を表示するタスクを定義した。

// レジスタ値表示用タスク
task dispmsg;
input [8*8:1] msg;

begin
$display("%0t  PC=%d(0x%h), N=%b, Z=%b, C=%b, V=%b, R0=%h, R1=%h, R2=%h, R3=%h %s#", $time, PC, PC, N, Z, C, V, R0, R1, R2, R3, msg);
end
endtask

これらを用いて以下のようにしてassertを定義した。

// アサーションタスク。assert_condが偽のときエラー。次の命令終了まで待つ。
task assertmsg;
input assert_cond;
input [8*8:1] msg;

begin
if(~assert_cond) begin
err = 1;
$display("# %s: assertion FAILED ###################################################", msg);
end
dispmsg(msg);
@(posedge fetch_start)
#1;
end
endtask
// アサーションタスクのメッセージなし版
task assert;
input assert_cond;
assertmsg(assert_cond, "");
endtask

引数のassert_condが真であることを期待している。よってもし偽であればエラーメッセージを出力する。
その後、waitpcタスクと同様の方法で、次の命令フェッチまで時間待ちをする。
これにより、assertタスクを並べていくだけで、各命令ごとにその実行結果を確認できる。

これらをどう使うかというと、次のようにする。
以下はテスト対象のプログラムの例。
今のところハンドアセンブルするしかない。


000_00055 // MV R0,R0+#0x55
000_000FF // MV R0,R0-#1

以下のようにして、パラメータファイルをメモリに読み込む。

$readmemh("prog.txt", uut.bus.ram1);

以下がテストベンチの例。

// テスト開始。
// プログラムprog.txtの9行目の実行結果についてassertでチェック
// したい。そのためには、プログラムが10行目を実行中のタイミング
// であること。10行目のアドレスは10で、そのときPC=11なので、
// PC==11 になるまで待つ必要がある。
waitpc(11);

// MV R0,R0+#0x55
assert(PC == 11 && R0 == 32'h55);
// MV R0,R0-#1
assert(R0 == 32'h54);

先ずwaitpcでテストしたい命令のアドレスまでシミュレーションを進める。
次にassert でチェックする。R0は必ず0に初期化されているので、
MV R0, R0+#0x55 によって、R0の値は55になっているはず。
次の命令では1デクリメントしているので54になっているはず。
一つ目のassertの中で、次のフェッチまで時間待ちをしているので、
2つ目のassertで次の命令の結果を確認できる。

あとは、prog.txtにいろんな命令パターンを追加して、
それらを確認するassertコードをテストベンチt_top.vに追加していけばいい。


テストケースを70個ぐらい作り、いくつかの命令パターンをテストした。
しかしまだまだテストしてない命令があるので、まだまだ苦労しそうだ。

0 件のコメント:

コメントを投稿