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 を新たに追加した。

0 件のコメント:

コメントを投稿