2011年11月29日火曜日

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


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


ちょっと比較をしてみると、かなりショッキングな結果に。
比較対象は
(a) 今回設計した(まだ設計途中の)とりあえず動作するだけの32bit CPU - TD32
(b) Zilinx 純正、spartan-3用 pico blaze (リンク先はログインが必要かも)
(c) KX_MIPS
(e) MR16

名前、動作周波数 (spartan-3E starter kitでの動作速度の目安)、使用スライス数

TD32、 27.5MHz、 3209 (I/O含む)
picoblaze、 100MHz、96
KX_MIPS、 57MHz、852 (ver.0.1は335)
kp4、 84MHz、32
MR16、 32MHz、595 (I/O含む)

ま、まぁ、勉強のため始めたんだし、消費リソースを減らす努力を何もしてないし、
。。。どう言い訳しても、リソースをたくさん消費して、動作速度は遅くて、
重要な機能が未実装で、他のCPUとなんの互換性もなく、
TD32はどう見てもダメダメですね。

さすがにpico blaze は優秀。非常にコンパクトで、しかも速い。
勉強のためKX_MIPSやMR16のソースを読んでみたい。
とは言え、一応完成させたいので引き続き


を年内に達成するべくもう少し努力します。

2011年11月27日日曜日

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


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


製作中のCPUを実際のボードで動かそうとしたら、
まだバグがあった。

TD4のときは、SW3を連続モード・手動モードの切り替えスイッチにして、
BTN_WESTをリセット、BTN_EASTを手動モードの1ステップ実行ボタンに
割り当てた。さらに、今回p1_led.txt に書いたテストプログラムでは、
最初にLEDが右端に1個点灯させ、BTN_SOUTHが押されている間は
左に動くというものを作った。時間待ちがないので、猛烈な速さで流れるため、
目で追うことはできず、1ステップ実行で確認した。

しかし、どうも1ステップ実行がうまく動作しない。
調べてみると、CPUを1命令だけ実行するために使っているcpu_en 信号の
周りでバグがあった。

このバグを修正すると、ちゃんと動作するようになった。

ISEで "Systhesize - XST" という最初のコンパイルをすることで、
設計した回路の最高動作速度が分かる。
CPUの動作速度はエリア優先の最適化オプションの条件で 27.550MHz
となった。コンパイルオプションをスピード優先に変えると 38.097MHz
となった。

Spartan starter kitの外部のクロックは50MHzなので、
DCM で 50MHz * 11 / 20 = 27.5MHz のクロックを作ってエリア優先の
範囲ではぎりぎりの周波数で動作させた。

50MHzで動いて欲しかったが、いろいろと複雑になってしまったので
仕方ない。

あとやり残したことは、


2011年11月25日金曜日

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


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


veritak で開発してきたが、ここらで一度FPGAボードで動かしてみたい。
そう思ってISE上に移してみて今更気づいたことがある。
FPGAのRAMは同期式が基本だろう。非同期RAMも使えるらしいが、
周辺回路はRAMだけではなく、I/Oなどもあるので同期で設計したほうがいい。
v9のモジュール宣言は以下のようになっていた。


コード: 
module td32( input wire clk, // システムクロック入力 input wire rst, // 同期リセット入力 output wire CYC_O, // バスサイクル出力(常にstbを出力する) output reg stb, // バスストローブ出力 output reg we, // 書き込みイネーブル出力 input wire ack, // バスからの応答入力 output reg [31:0] addr, // アドレスバス出力 input wire [31:0] indata, // データバス入力 output reg [31:0] outdata, // データバス出力


出力はすべてregで宣言している。このため、出力信号はクロックに同期している。
しかしこれを同期RAMに接続すると、必ず1クロックの遅れが生じ、
1命令の実行に必ず2クロックかかってしまう。
WISHBONE 仕様は守りつつ、かつウェイトなしでRAMにアクセスできるようにしたい。

メモリ側が同期で動作する以上、CPU側は非同期ですぐに応答しなければならない。
t0, t1, t2, ... をクロックの立ち上がりのタイミングとすると、例えば

t0: リセット入力rst が 1⇒0 となる。
t1: CPUがrst=0 を認識。開始アドレス0を出力、stb=1 を出力。
t2: メモリがstb=1 を認識。開始アドレス0を取り込み、対応するデータを出力。
ack=1を出力。
t3: CPU がack=1 を認識。開始アドレス0のデータ(=命令コード)を取り込み、
次のアドレス4を出力、stbは1のまま。
メモリはack=0を出力。

t4: メモリがstb=1 を認識。開始アドレス0を取り込み、対応するデータを出力。
ack=1を出力。

という具合に進んでいくと、これでもまだ 1命令あたり2サイクルかかる。
連続して実行するには、t1の直後にCPUがstb=1とした段階で、
メモリ側はすぐ応答できる見込みのもとで直ちに ack=1 として、
t2 の段階でデータを出力しなければならない。
CPUはt2の直後に出力されたデータを取り込んで直ちに命令実行、
さらに次の命令のアドレスを出力して、t3でメモリがそのアドレスを
取り込めるようにしなければならない。

以上を踏まえて、タイミングを設計する。

CPU内部で必要なフリップフロップを洗い出したところ、以下の結果になった。

reg stb, // バスストローブ出力

reg cycfe, cycim, cycrd, cycrd1, cycrd2, cycwr; // CPUサイクル
reg [31:0] instr_reg;
reg fn; // 符号フラグ
reg fz; // ゼロフラグ
reg fc; // キャリーフラグ
reg fv; // オーバフローフラグ
reg fcnz; // カウンタ・ノンゼロ・フラグ
reg exec_reg;
reg [31:0] gr[0:15]; // 汎用レジスタ
reg [31:0] indata_d; // 入力データバス記憶用

instr_reg はフェッチサイクルで読み込んだインストラクションを記憶するもので、
1命令で複数のCPUサイクルが必要な命令では必要となる。
exec_reg は条件付き実行フラグを評価した結果、命令を実行するか否かを
記憶するもので、同様に必要となる。
indata_d を使うのは opxxi のときだけで、左オペランドの [Ri]+ の読み込み内容を
次のサイクルまで記憶するために必要である。

いずれも必要不可欠なフリップフロップであり、これら以外は組み合わせ回路とする。
そのように変更して veritak にかけたところ、veritak が暴走?
シミュレーションが止まらなくなった。
veritak の FAQページを見ると、はっきりと書いてあった。

FAQ 18. 「Simulationが進まない。」
Ans. 「無限ループ記述をチェックしてみてください。 Alaways/Initialで記述される無限ループは、DebugモードPauseで止まるはずですが、Net記述で時間の進まない無限ループだと 止まらない仕様になっております。Simulationが進まなくなる時刻手前からStep実行により記述のデバッグを試みてください。

つまり、どこかで無限ループができてしまっている。
これの調査で苦労したが、結果を言うと、WISHBONE 仕様に従って
複数のRAMやI/Oを共有バスに接続するためのインターコネクトを
以下のように実装したことが問題だった。

コード: 
// interconn. parameter [31:0] RAM1_BASE = 32'h00000000; parameter [31:0] RAM2_BASE = 32'h00000800; parameter [31:0] SW_BASE = 32'h00001000; parameter [31:0] LED_BASE = 32'h00001004; wire sel_ram1 = (ADR_O[31:12] == RAM1_BASE[31:12]); wire sel_ram2 = (ADR_O[31:12] == RAM2_BASE[31:12]); wire sel_sw = (ADR_O[31:0] == SW_BASE[31:0]); wire sel_led = (ADR_O[31:0] == LED_BASE[31:0]); wire stb = CYC_O & STB_O; wire stb_ram1 = stb & sel_ram1; wire stb_ram2 = stb & sel_ram2; wire stb_sw = stb & sel_sw; wire stb_led = stb & sel_led; wire [31:0] outdata_ram1, outdata_ram2; wire [7:0] outdata_sw; wire ack_ram1, ack_ram2, ack_sw, ack_led; assign DAT_I =  sel_ram1 ? outdata_ram1 : sel_ram2 ? outdata_ram2 : sel_sw ? { 24'b0, outdata_sw } : 0; assign ACK_I =  sel_ram1 ? ack_ram1 : sel_ram2 ? ack_ram2 : sel_sw ? ack_sw : sel_led ? ack_led : 0;


周辺回路を選択する信号 sel_xxx は、アドレスに基づいて得られる。
周辺回路からの出力データ outdata_xxx は、sel_xxx に基づいて合成してCPUに返される。
するとCPUの入力データはsel_xxx に依存し、sel_xxx は、アドレスに依存し、
アドレスは命令コードに依存し、命令コードは入力データに依存している。
ここでループが生じている。

上記のインターコネクト回路は、WISHBONE仕様書に掲載されていた
以下の回路を参考にしたのだが。


問題が分かったので、sel_xxx をフリップフロップで以下のように遅らせて解決した。


コード: 
// interconn. parameter [31:0] RAM1_BASE = 32'h00000000; parameter [31:0] RAM2_BASE = 32'h00000800; parameter [31:0] SW_BASE = 32'h00001000; parameter [31:0] LED_BASE = 32'h00001004; wire sel_ram1 = (ADR_O[31:11] == RAM1_BASE[31:11]); wire sel_ram2 = (ADR_O[31:11] == RAM2_BASE[31:11]); wire sel_sw = (ADR_O[31:0] == SW_BASE[31:0]); wire sel_led = (ADR_O[31:0] == LED_BASE[31:0]); wire stb = CYC_O & STB_O; wire stb_ram1 = stb & sel_ram1; wire stb_ram2 = stb & sel_ram2; wire stb_sw = stb & sel_sw; wire stb_led = stb & sel_led; wire [31:0] outdata_ram1, outdata_ram2; wire [7:0] outdata_sw; wire ack_ram1, ack_ram2, ack_sw, ack_led; reg sel_ram1_d, sel_ram2_d, sel_sw_d, sel_led_d; always @(posedge CLK_I) begin { sel_ram1_d, sel_ram2_d, sel_sw_d, sel_led_d } <= { sel_ram1, sel_ram2, sel_sw, sel_led }; end assign DAT_I =  sel_ram1_d ? outdata_ram1 : sel_ram2_d ? outdata_ram2 : sel_sw_d ? { 24'b0, outdata_sw } : 32'hx; assign ACK_I =  sel_ram1_d ? ack_ram1 : sel_ram2_d ? ack_ram2 : sel_sw_d ? ack_sw : sel_led_d ? ack_led : 1'bx;



もう一つ、これまでちゃんと考えて来なかったCPUのリセットの仕組みについて考えてみた。
CPUからの出力のフリップフロップを取り除いた関係で、リセット直後の動作がおかしくなった。


以下は問題を修正した後のリセット周りのタイミングを示している。

黒い線がt0, M1と書かれた緑の線がt1 である。
メモリが開始アドレス0を取り込んで、最初の命令コードを出力するのは
右から2個目の緑の線t2 である。
メモリは単にstbをそのままackとして返しているので、常にstb=ackとなる。
t0~t2の間は、メモリから不正なデータを受け取るので、この間CPUは処理を
進めてはならない。このことを確認するためのフラグとして initdone を新たに設けた。
initdone==0 の間は、indataを強制的に0 (NOP) にすることで、余計な処理を
させないようにする。

以下の図は、ウェイトを挿入してメモリの応答を遅らせた場合である。


この場合、t0~t3 の間はメモリはまだ応答していない。(図ではt2で何か出力しているが)


以上により、CPUは基本的には同期で動作しているが、
ackへの応答やデータの取り込みは非同期に直ちに
取り込んで命令を実行するようになった。
かつ、遅い周辺回路はウェイトを挿入することもでき、
周辺回路が十分に速ければ毎クロックCPUサイクルを進められる。
リセット直後の問題も解消し、実行開始を示す信号 initdone を新たに追加した。

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


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


さてさて、ここらで考えられるCPUの機能を思いつくまま挙げてみる。(チェック・リスト)
  1. 任意アドレスのメモリ読み込み・書き込み
  2. レジスタに定数を設定、レジスタ間のデータ移動
  3. 算術演算、論理演算、シフト演算、キャリー付き加減算
  4. 条件分岐、無条件分岐(相対アドレス/絶対アドレス)
  5. スタックポインタ(SP)とpush/pop
  6. サブルーチンコール、リターン
  7. 割り込み、割り込みからの復帰、割り込み禁止・解除
  8. フラグなど、CPU状態の保存・復帰(マルチスレッド・マルチタスク実現)
  9. テスト&セット命令 (mutex実現のため)
  10. ブレークポイントのための命令。1ワードでソフト割り込みするなど
  11. バイト単位、16bit単位のアクセス、符号拡張命令
  12. SPに定数を加減算、SP相対アドレッシング(高級言語のローカル変数実装)
  13. 分岐テーブルに基づくジャンプ(オブジェクト指向言語のメソッド継承実装)
  14. forループ、break、switch文、関数呼び出しなどの実装に便利な機能など
  15. 乗除算、浮動小数点演算、多倍長演算、もしくはその効率的実装
  16. BCD演算の効率的実装
  17. 高速なブロック転送
  18. バレルシフタ
  19. 複数レジスタの一括push/pop
  20. 大量データの一括演算(MMX命令みたいな)
  21. 保護モードとソフトウェア割り込み
  22. 保護モード用のレジスタの効率的な退避・復帰(仮想マシンの実装)
  23. 仮想メモリ、メモリの保護
  24. HALT命令(低消費電力対応など)
1~10 まではほぼ必須の機能。11以降はあればうれしい、という感じ。
それぞれ、今考えているv9のcpuで出来るかどうか考えてみる。

1. 任意アドレスのメモリ読み込み・書き込み
⇒ ○
以下のopraとoparでできる。

wire opra = (opaddr == 1); // Rd = Ri op [im32]
wire opar = (opaddr == 5); // [im32] = Ri op (Rj+im8)
MV Rd, [im32]
MV [im32], Rd

2. レジスタに定数を設定、レジスタ間のデータ移動

⇒ ○
直接的に定数を設定する機能はないが、定数をどこかのメモリに設定しておいてopraでレジスタに設定できる。
MV Rd, [im32]
定数が-128~127の範囲内であれば、RSB演算と組み合わせて以下のように1命令で実行できる。

// 8: RSB  Rd = Rj - Ri       オペランド逆転減算
RSB Rd, Ri, (Rj+im8) ⇒ Rd = (Rj+im8)-Ri なので、Ri=Rj にすると Rd = im8 になる。

レジスタ間の移動は下記のoprrでできる。


wire oprr = (opaddr == 0); // Rd = Ri op (Rj+im8)
MV Rd, Rj


3. 算術演算、論理演算、シフト演算、キャリー付き加減算
⇒ ○
16種類の演算に含まれている。

4. 条件分岐、無条件分岐(相対アドレス/絶対アドレス)
⇒ ○
R15がプログラムカウンタなので、
MV R15,[im32] 
がジャンプ、
MV R15,(R15+im8) 
が相対ジャンプになる。ここでも、できれば MV R15, im32 とイミディエイトアドレッシングで直接値を指定したいところだがそのような命令はない。定数テーブルが命令アドレス±512バイトの範囲内であればoprxを使って
wire oprx = (opaddr == 2); // Rd = Ri op [Rj+im8*4]
MV R15, [R15+im8*4] 
により定数を取り込める。多くの場合は相対ジャンプで済むので、あまり問題無いだろう。
条件分岐にするには、条件付き実行フラグを利用すればよい。



5. スタックポインタ(SP)とpush/pop
⇒ ○
下記のoprxi, opxrd を使うことで、任意のレジスタをスタックポインタとして利用できる。
push Rn をするには MV [R12-#4]-, Rn で、pop Rn は MV Rn, [R12]+ で実行できる。
wire oprxi = (opaddr == 3); // Rd = Ri op [Rj+im8*4]+
wire opxrd = (opaddr == 7); // [Rd+im8*4]- = Ri op Rj

6. サブルーチンコール、リターン
⇒ △
超重要機能だが、サブルーチンコールのための機能がない。
一応、以下の3命令で実行できる。
RSB R0, R0+#4, R0
ADD [R12-#4]-, R0, R15
MV R15, [R15+im8*4]  又は  MV R15, [im32] など
リターンは
MV R15, [R12]+
となる。条件付きのサブルーチンコールもできる。
普通のCPUなら1命令でできるサブルーチンコールが3命令になってしまう上に、
R0などの関係ないレジスタが一つ必要になる。
2つ目のADD [R12-#4]-, R0, R15は、リターンアドレスをpushしている。
単にpush R15なら1命令でできるが、これだと戻りアドレスが4少なくなってしまう。
これはなんとかしないと、使いづらいだろう。

7. 割り込み、割り込みからの復帰、割り込み禁止・解除
⇒ ×

8. フラグなど、CPU状態の保存・復帰(マルチスレッド・マルチタスク実現)
⇒ ×

9. テスト&セット命令 (mutex実現のため)
⇒ ×
以下のopxxiを使えば、1命令で読み込みと書き込みができる。
後はフラグの使い方とうまい演算を組み合わせれば実現できそうだ。

wire opxxi = (opaddr == 4); // [Rd]+ = [Ri]+ op [Rj+im8*4]+


10. ブレークポイントのための命令。1ワードでソフト割り込みするなど
⇒ △
1命令でジャンプするには、RSB命令で8bitイミディエイトデータを取り込む。
wire oprr = (opaddr == 0); // Rd = Ri op (Rj+im8)
RSB R15, R15, (R15+im8)
アドレス0~127の32ワードがソフト割り込みベクタのようになるが、
これらのアドレスには32bitジャンプ命令を置く必要があるため、
実質的にはソフト割り込みは16個しか使えない。
アドレス-128~-1も活用できる。これらのアドレスは0xFFFFFF00~0xFFFFFFFF
に位置する。
後述のようにR0をゼロ・レジスタにすれば、

wire oprx = (opaddr == 2); // Rd = Ri op [Rj+im8*4]
MV R15, [R0+im8*4]
により、より直接的に-512~+511の256ワードをソフト割り込みベクタに利用できる。
ただし、ソフト割り込みとして利用するには自動的に割り込みを禁止する機能も
必要になるかも知れない。
そうしないと、ソフト割り込み直後に割り込みが二重にかかる危険がある。

11. バイト単位、16bit単位のアクセス、符号拡張命令
⇒ ×
PCは32bitあり、アドレス出力にはこのまま32bitが出力されている。
32bitアドレスにアクセスする以下のアドレッシングモードにおいても、
下位2bitはそのままアドレス信号に出力されている。

wire opra = (opaddr == 1); // Rd = Ri op [im32]
wire opar = (opaddr == 5); // [im32] = Ri op (Rj+im8)
従って、4で割り切れないアドレスにアクセスしたときにこれらの意味合いを
持たせるように使うことはできる。
しかし、上位24ビットないし上位16ビットをマスクしたり、
符号拡張したりする命令はないので、個別に実装しなければならない。


12. SPに定数を加減算、SP相対アドレッシング(高級言語のローカル変数実装)
⇒ ○
SPは汎用レジスタなので、普通にできる。

13. 分岐テーブルに基づくジャンプ(オブジェクト指向言語のメソッド継承実装)
⇒ ○
テーブルアドレスをRjに設定しておけば、以下の命令で分岐テーブルに基づくジャンプになる。
wire oprx = (opaddr == 2); // Rd = Ri op [Rj+im8*4]
MV R15, [Rj+im8*4]

14. forループ、break、switch文、関数呼び出しなどの実装に便利な機能など
⇒ ?
後日、コンパイラを作ってみて検証したい。

15. 乗除算、浮動小数点演算、多倍長演算、もしくはその効率的実装
⇒ △
乗算だけは入れてみた。まだ試してないが。

16. BCD演算の効率的実装
⇒ ×

17. 高速なブロック転送
⇒ ○
あまり高速ではないが、一応、ループ1回あたり2命令でできる。

18. バレルシフタ
⇒ ×

19. 複数レジスタの一括push/pop
⇒ ×
サブルーチンの実装や、マルチスレッドのタスク切り替えなどに使える魅力的な機能。

20. 大量データの一括演算(MMX命令みたいな)
⇒ ×

21. 保護モードとソフトウェア割り込み
⇒ ×
Linuxのような本格的なOSを動かすには必須の機能。
でもこのCPUには贅沢な機能だろう。

22. 保護モード用のレジスタの効率的な退避・復帰(仮想マシンの実装)
⇒ ×

23. 仮想メモリ、メモリの保護
⇒ ×

24. HALT命令(低消費電力対応など)
⇒ ×


必須の機能のうち、まだないものは下記がある。

サブルーチンコール、リターン
割り込み、割り込みからの復帰、割り込み禁止・解除
フラグなど、CPU状態の保存・復帰(マルチスレッド・マルチタスク実現)
テスト&セット命令 (mutex実現のため)
ブレークポイントのための命令。1ワードでソフト割り込みするなど

後日、よく検討しよう。

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個ぐらい作り、いくつかの命令パターンをテストした。
しかしまだまだテストしてない命令があるので、まだまだ苦労しそうだ。