VerilogでCPU設計入門
トラ技の付録基板(2006年4月号)で、CPUを作ってみましょう。ところで、このデバイスALTERA MAXU、EPM240T100C5は、240セルしかないので、正攻法では、どうがんばっても載りません。フラッシュも載っているのですが、これをROMにしてしまうと、かなり遅くなるし、使い方が難しいです。このセル数は、常識的には、数個のカウンタで埋まってしまいます。そこで、次のような方針で設計しました。
という方針で設計しました。フラッシュを使用していませんので、トラ技デバイスでは、16ワード位しか載りません。それでも、例えば、次のようなパターンを発生させることができます。(また、ROM容量が許せば、64KWORDまで拡張可能なアーキテクチャになっています。)
また、ビジュアルに内部の状態をブレークポイント付で確認することが出来ます。(例によってGUIは、C#で作成しています。)
これにより、容易にCPUの中身の動きを知ることができます。
MAX2にレイアウト後の遅延シミュレーションでも動作確認しています。(ヒゲは、遅延が一定していないためです。)
<CPUとは、順序機械>
一番単純な順序機械といえば、カウンタです。カウンタは、順番を生成しますので、それにROMをつければ、コントロール付きの順序機械になります。
下図で、カウンタをPC=プログラムカウンタと言うと格好よくなります。さて設計したCPUは、32ビット幅があるので、ROMの幅は32ビットになります。
そのままでは、なにかを判断して処理を変えるということができないので、PCをなんらかの形で制御する必要があります。また、その為には、一時的な記憶素子が必要で
す。記憶素子をレジスタの集まりとしてまとめたものをレジスタファイルと言います。上のレジスタファイルは、同時に読み書きできるポートが3つあります。ライトポートが一つ、リードポートが二つです。また、レジスタ数は、16ビットが4個です。
一番目のゼロレジスタは、RISCでは、定番です。最後のポートレジスタは、このCPUだけの特殊仕様です。(リソース削減のため)
名称 | 機能 |
ゼロ | WriteしてもReadしても0、なので実体は、ただのGND |
R0 | 汎用レジスタ |
R1 | 汎用レジスタ |
ポート | 特殊レジスタ、そのまま、外部ポート出力になる |
HDLソースで見てもらったほうが分かりやすいかもしれません。
module register_file(input clock,write,reset, input [1:0] A_address, input [1:0] B_address, input [1:0] write_address, input [15:0] alu_data,port_in_data, output [15:0] out_dataA, out_dataB,port_out_data); reg [15:0] port_reg,r0,r1; always @(posedge clock,posedge reset) begin if (reset) begin port_reg<=0; r0<=0; r1<=0; end else if (write) begin case (write_address) `PORT: port_reg<=alu_data; `R0: r0<=alu_data; `R1: r1<=alu_data; //default r0<=alu_data; endcase end end assign out_dataA=A_address==`PORT ? port_in_data : A_address==`R0 ? r0 : A_address==`R1 ? r1 : 0; assign out_dataB=B_address==`PORT ? port_reg : B_address==`R0 ? r0 : B_address==`R1 ? r1 : 0; assign port_out_data=port_reg; endmodule
さて、レジスタファイルの後段は、ALUです。本格的なALUを実装しています。
module alu (input [3:0] command, input [2:0] jump_cond, input [15:0] portA,portB, output reg [15:0] alu_out); always @* begin case (command) `ADD_com : alu_out=portA+portB; `SUB_com : alu_out=portA-portB; `SHIFT_L_com: alu_out=portB >>1; `SHIFT_R_com: alu_out=portB <<1; `JMP_com : case (jump_cond) `Eq : alu_out=portA==portB; `Not_Eq:alu_out=portA !=portB; `Always : alu_out=1; `Always_Not :alu_out=0; `LT : if (portA < portB) alu_out=1; else alu_out=0; `GT : if (portA > portB) alu_out=1; else alu_out=0; default: alu_out=0; endcase default: alu_out=portA+portB; endcase end endmodule
後は、PCのコントロールです。これは、後述するパターンジェネレータとしての機能が入っているために、ちょっとだけ複雑ですが、たいしたことはありません。
斜め字を無視して読むと、JUMPコマンドのとき、ALU出力のビット0が1だったら、immediate
data の番地をロード、それ以外は、pc<=pc+1 ;つまり、ただのカウンタということです。
このimmediate_data は、32ビット幅のROMの下位16ビットに割り当てています。ですから、ROMの容量さえ許せば、64KWORDまで、どこにでも飛ばすことができます。
//control of PC always @(posedge clock, posedge reset) begin if (reset) pc<=0; else if (command==`JMP_com && alu_data[0]==1'b1) pc<=immediate_data;//Jump if jump condition is met. else if (command==`SYNC_com) begin//Sync Start Condition if (sinc_pos_edge_detected) pc<=pc+1;//Exectute Next Address! else pc<=pc;//Hold until sync pulse is detected. end else if (loop_flag ) begin//Loop Condition if (loop_counter==0) pc<=pc+1;//Execute Next Address! else pc<=pc;//Hold until loop counter becomes zero. end else pc<=pc+1;//else increment PC end
基本的には、以上の要素をつなげれば、CPUになります。すごく簡単ですね。勿論1CLKで、1INSTRUCTION処理でMUX2デバイスでも30MHz以上で動作可能です。
さて、ROM出力は、32ビットの内16ビットを飛び先に使いました。
残りのフィールドを含む割付は次のようになっています。
ビット位置 | ビット幅 | 名称 | 内容 |
31:28 | 4 | command | コマンド |
27:26 | 2 | A_address | レジスタファイルAポートのアドレス |
25:24 | 2 | B_address | レジスタファイルBポートのアドレス |
23:22 | 2 | write_address | レジスタファイルライトポートのアドレス |
21:19 | 3 | jump_cond | ジャンプ条件 |
18:16 | 3 | reserved | |
16:16 | 1 | loop_flag | ループ処理するかどうかのフラグ、1でループ処理 |
15:0 | 16 | immediate | 即値または、飛び先 |
命令の内容を解凍するデコードという処理はありません。ですので、32ビットという広いビット幅が要るのですが、その分簡単に設計できます。
このようにROMの出力を制御線に使うやり方をマイクロコード方式と呼びます。このCPUは、マイクロコード自体が、機械語命令になっています。
このCPUがNativeで、もっている命令は、次です。
`define ADD_com 4'b0000 `define LOAD_IM_com 4'b0001 `define SUB_com 4'b0010 `define SHIFT_L_com 4'b0011 `define SHIFT_R_com 4'b0100 `define JMP_com 4'b0101 `define SYNC_com 4'b0110
よくアセンブラで見かけるMOV命令がありません。これは、RISCの定番である、ゼロレジスタとの加算で実現しています。
ソースでは、マクロ定義で、ビットフィールドを32ビットのROMコードに変換しています。
`define MOV( r0, r1) {Add,r0,ZR, r1,Eq,Reserved,Zero_Im}
上のコードの意味は、
r1 <=r0 +0
になります。
同様に、マクロで命令を定義することができます。アセンブラでよく見かけるNOPは、どう実装したらよいでしょうか?実は、ALL 0は、NOPにしています。
その意味は、
ゼロレジスタ<=ゼロレジスタ+ゼロレジスタ
です。
parameter [1:0] Port=`PORT,//IO PORT R0= `R0, R1= `R1, ZR= `ZR;//ZERO REGISTER parameter [15:0] Zero_Im=16'h0000; parameter [3:0] Load_Im=`LOAD_IM_com, Add=`ADD_com, Sub=`SUB_com, Shift_L=`SHIFT_L_com, Shift_R=`SHIFT_R_com, Jmp=`JMP_com, Sync=`SYNC_com; parameter [2:0] Eq=`Eq, Not_Eq=`Not_Eq, Always=`Always, Always_Not=`Always_Not, LT=`LT, GT=`GT, LTE=`LTE, GTE=`GTE; parameter [2:0] Reserved=3'b000, Loop_Enabled=3'b001; `define MOV( r0, r1) {Add,r0,ZR, r1,Eq,Reserved,Zero_Im} `define CLR( r0) { Add,ZR,ZR,r0,Eq,Reserved,Zero_Im} `define JUMP(num) {Jmp,ZR,ZR,ZR,Always,Reserved,16'd num} `define LOAD_IM(r0,num) {Load_Im,Port,ZR,r0,Eq,Reserved,16'd num} `define ADD(r0,r1,r2) { Add,r0,r1,r2,Eq,Reserved,Zero_Im} `define SUB(r0,r1,r2) { Sub,r0,r1,r2,Eq,Reserved,Zero_Im} `define JUMP_IF_LESS_THAN(r0,r1,num) {Jmp,r0,r1,ZR,LT,Reserved,16'd num} `define JUMP_IF_GREATER_THAN(r0,r1,num) {Jmp,r0,r1,ZR,GT,Reserved,16'd num} `define JUMP_IF_EQ(r0,r1,num) {Jmp,r0,r1,ZR,Eq,Reserved,16'd num} `define SHIFT_L(r0,r1) {Shift_L,ZR,r0,r1,Eq,Reserved,Zero_Im} `define SHIFT_R(r0,r1) {Shift_R,ZR,r0,r1,Eq,Reserved,Zero_Im} `define SYNC {Sync,ZR,ZR,ZR,Eq,Reserved,Zero_Im}
ここまで、読んでくれてありがとうございます。そうです。アセンブラはないのです。
ROMコードは、例えば、上で定義したマクロを使って次のように書きます。
これぞ、究極の「コンピュータの原理を学ぶ」 かもしれません。 こういう命令が欲しいというのがあったら、上でマクロ定義してしまえば、よい訳です。
マクロ展開は、Veritakオプションで、preout.v として見れますので、思い通りに展開されているかチェックしてみてもよいでしょう。
function [31:0]romdata; input [4:0] address; case (address) //Commad A_addr, B_addr, Write_addr, Jmp_cond,Reserved, IM 0: romdata =`LOAD_IM(R0,1); 1: romdata =`CLR(Port); 2: romdata =`Add_with_loop(Port,R0,100); 3: romdata =`CLR(Port); 4: romdata =`LOAD_IM(R0,2); 5 : romdata =`Add_with_loop(Port,R0,50); 6: romdata =`CLR(Port); 7: romdata =`LOAD_IM(R0,4); 8 : romdata =`Add_with_loop(Port,R0,25); 9 : romdata =`LOAD_IM(Port,75); 10: romdata =`Add_with_loop(Port,ZR,25); 11: romdata =`LOAD_IM(Port,51); 12: romdata =`SYNC; 13: romdata =`LOAD_IM(R1,44000); 14: romdata =`JUMP_IF_LESS_THAN(R1,Port,1); 15 : romdata =`SHIFT_R(Port,Port); 16: romdata =`JUMP(14); // 3 : romdata =`LOAD_IM(R0,100); // 4 : romdata =`LOAD_IM(R1, 62444); // 5 : romdata =`ADD(R0,R1,Port); // 6 : romdata =`LOAD_IM(Port,10000); // 7 : romdata =`ADD(R0,R1,R0); // 8 : romdata =`SUB(R0,R1,R0); // 9 : romdata =`ADD(Port,R0,R0); // 10 : romdata =`JUMP_IF_LESS_THAN(R0,R1,0); // 11 : romdata =`JUMP_IF_EQ(R0,R1,3); 17 : romdata =0; 18 : romdata =0; 19 : romdata =0; 20 : romdata =0; default : romdata=0; endcase endfunction
<Confiugurable CPU>
このCPUで、設計していて面白いことに気づきました。それは、このCPUは、ROMワードいくつまでMAX2に載るとは、言えないことです。実際にROMで使う命令によります。ROMをメモリとしてではなく、LUTで構成しているので、実装する命令で影響を受けます。つまり、ALUでは、いろいろ定義してあるのですが、論理合成で、使わないロジックは、容赦なくRudctionされます。マジックの種明かしでした。
<パイプライン化ということ>
このCPUは、長いパスを持っています。多分、PCに始まりROM ->Register FILE −>ALU−> PC コントロールロジックー>PCに来るパスが最長だろうと思います。
最長のパスをクリティカルパスと言います。パイプライン化した普通のCPUでは、このパスは何段かのFF間の組み合わせ回路に分割されることになります。
CPUの応用
<パターンGenerator>
ループフラグをONにすると、ループカウンタが0になるまで、その場所に留まり、命令を繰り返します。これは、Shift演算にも活用することができます。
//loop command `define Shift_Right_with_Loop(r0,num) {Shift_R,ZR,r0,r0,Eq,Loop_Enabled,16'd num } `define Add_with_loop(r0,r1,num) { Add,r1,r0,r0,Eq,Loop_Enabled,16'd num} //Use Bport for port_reg
ループカウンタは、即値と兼用ですので、全命令で適用できる訳では、ありません。しかし、任意時間幅のパターンを作る場合には、この性質は、重宝します。 また、SYNC命令で、POS EDGEで命令スタートということもできます。BINARYのシリアルパターンを作る場合は、16ビットごとに、CPUへCLCOCKを与えれば、シリアルパターン生成器にもなります。 <VPI> 今回も vpi_dll5.dll に追加しました。$SendMessageと、$MessageBox を追加しました。その名の通りですが、詳細はソースを参照ください。 今回追加したソースは次です。
登録部
//Jul.11.2006 tf_data.type =vpiSysFunc;// tf_data.sysfunctype =vpiIntFunc;// tf_data.tfname = "$SendMessage"; tf_data.user_data = "$SendMessage"; tf_data.calltf = sys_SendMessage; tf_data.compiletf = 0; tf_data.sizetf = sys_systems_size_tf;//func vpi_register_systf(&tf_data); tf_data.type =vpiSysFunc;// tf_data.sysfunctype =vpiIntFunc;// tf_data.tfname = "$MessageBox"; tf_data.user_data = "$MessageBox"; tf_data.calltf = sys_MessageBox; tf_data.compiletf = 0; tf_data.sizetf = sys_systems_size_tf;//func vpi_register_systf(&tf_data);
実装部
static int sys_SendMessage(char* name) { vpiHandle systfref, argsiter, argh; s_vpi_value value; systfref = vpi_handle(vpiSysTfCall, NULL); /* get system function that invoked C routine */ argsiter = vpi_iterate(vpiArgument, systfref);/* get iterator (list) of passed arguments */ unsigned message_parameter[4]; for (unsigned i=0;i<4;i++){ argh = vpi_scan(argsiter);/* get the one argument - add loop for more args */ if(!argh){ vpi_printf("$VPI sys_SendMessage: missing parameter. \n"); // vpi_sim_control(vpiFinish, 1); return 0; } value.format = vpiIntVal; vpi_get_value(argh, &value); message_parameter[i]=value.value.integer; } unsigned result= ::SendMessage(reinterpret_cast<HWND>(message_parameter[0]),//handle message_parameter[1],//WM_COMMAND message_parameter[2],//WPARAM message_parameter[3]);//LPARAM if(argh) vpi_free_object(argsiter); value.value.integer =result;//; value.format = vpiIntVal;/* return the result */ vpi_put_value(systfref, &value, NULL, vpiNoDelay); return(0); } static int sys_MessageBox(char* name) { vpiHandle systfref, argsiter, argh; s_vpi_value value; systfref = vpi_handle(vpiSysTfCall, NULL); /* get system function that invoked C routine */ argsiter = vpi_iterate(vpiArgument, systfref);/* get iterator (list) of passed arguments */ unsigned message_id=0; string str_message; string str_caption; for (unsigned i=0;i<2;i++){ argh = vpi_scan(argsiter);/* get the one argument - add loop for more args */ if(!argh){ vpi_printf("$VPI sys_MessageBox: missing parameter. \n"); // vpi_sim_control(vpiFinish, 1); return 0; } value.format = vpiStringVal; vpi_get_value(argh, &value); if (i==0) str_message=value.value.str; else if (i==1) str_caption=value.value.str; } argh = vpi_scan(argsiter); if (argh) { value.format = vpiIntVal; vpi_get_value(argh, &value); message_id=value.value.integer; } unsigned result=MessageBox(0,str_message.c_str(), str_caption.c_str(),message_id); if(argh) vpi_free_object(argsiter); value.value.integer =result;//; value.format = vpiIntVal;/* return the result */ vpi_put_value(systfref, &value, NULL, vpiNoDelay); return(0); }
テストベンチのソースです。
Verilog HDLソースから、MessageBoxを開くことができます。
`timescale 1ns/1ps `ifdef GATE_SIM `define CYCLE 26.92 `else `define CYCLE 30 `endif module test; reg clock=0; reg reset=0; reg [15:0] port_in_data=0; reg sync=0; wire [15:0] port_out_data; always #(`CYCLE/2) clock=~clock; initial begin reset=1; #105; reset=0; #1000000; $finish; end cpu cpu(.clock,.reset,.sync,.port_in_data,.port_out_data); always @(negedge clock) begin if (port_out_data==51) begin repeat( 100) begin @(negedge clock); end sync=1; @(negedge clock); sync=0; end end localparam integer MB_YESNO=4; localparam integer MB_YES=6; localparam integer MB_NO=7; localparam integer TOP_MOST=32'h4_0000; `ifndef GATE_SIM integer handle,i; reg [8*20:1] str; reg [31:0] data; integer result; parameter integer WM_COMMAND=32'h111; initial begin handle=$FindWindow("MyCPU"); if (handle) begin end else begin :loop0 result=$MessageBox("ようこそ。Visual デバッガを起動しますか?","VeritakMessageBox",MB_YESNO | TOP_MOST ); if (result==MB_YES) begin $shell_execute("cpu"); repeat(100) begin//Retry LOOP; Repeat until MyCPU is detected. $Sleep(100);//100ms wait handle=$FindWindow("MyCPU"); if (handle) disable loop0; end end end if (handle) begin $SendMessage(handle,WM_COMMAND,256,0);//ROM コードを送出 for (i=0;i<32;i=i+1) begin data=cpu.rom.romdata(i); $SendMessage(handle,WM_COMMAND,i+256,data); end $MessageBox("GUIの準備ができました。必要ならMyCPUのROMリストで、ブレークポイントを設定してください。その後にOKボタンを押してください。","VeritakMessageBox"); end else begin //$MessageBox("MyCPUが起動できませんでした。","VeritakMessageBox"); end end always @(negedge clock) begin handle=$FindWindow("MyCPU"); if (handle) begin//現在の状態を送出 //regfile $SendMessage(handle,WM_COMMAND,1,cpu.rfile.out_dataA); $SendMessage(handle,WM_COMMAND,2,cpu.rfile.out_dataB); $SendMessage(handle,WM_COMMAND,3,cpu.rfile.alu_data); $SendMessage(handle,WM_COMMAND,4,cpu.rfile.r0); $SendMessage(handle,WM_COMMAND,5,cpu.rfile.r1); $SendMessage(handle,WM_COMMAND,6,cpu.rfile.port_reg); $SendMessage(handle,WM_COMMAND,7,cpu.ALU.portA); $SendMessage(handle,WM_COMMAND,8,cpu.ALU.portB); $SendMessage(handle,WM_COMMAND,9,cpu.ALU.alu_out); $SendMessage(handle,WM_COMMAND,10,cpu.ALU.command); $SendMessage(handle,WM_COMMAND,11,cpu.ALU.jump_cond); $SendMessage(handle,WM_COMMAND,12,cpu.loop_counter); //PC コマンドはラストに送る result=$SendMessage(handle,WM_COMMAND,0,cpu.pc); if (result==-1) begin//ブレーク要求がGUIから来たら $stop; end end end `endif integer handle_7led; initial begin handle_7led=$FindWindow("7LED");//を探す if (!handle_7led)begin :loop1 result=$MessageBox("ポート出力を7セグメントLEDパネルに出力しますか?","VeritakMessageBox",MB_YESNO | TOP_MOST ); if (result==MB_YES) begin $shell_execute("LED7SEG");//7セグメントLEDを立ち上げる repeat(100) begin//最長100msx100=10sec待ち $Sleep(100); handle_7led=$FindWindow("7LED");//を探す if (handle_7led) disable loop1;//見つかったら脱出 end end end end always @(negedge clock) begin handle_7led=$FindWindow("7LED"); if (handle_7led) begin :LED//現在の状態を送出 reg [31:0] led_value={1'b0,decoder(port_out_data[15:12]), 1'b0,decoder(port_out_data[11:8]), 1'b0,decoder(port_out_data[7:4]), 1'b0,decoder(port_out_data[3:0])}; $SendMessage(handle_7led,WM_COMMAND,led_value,0); end end function [6:0] decoder(input [3:0] din);//7segment LED case (din) 4'b0000 : decoder = 7'b1111110; 4'b0001 : decoder = 7'b0110000; 4'b0010 : decoder = 7'b1101101; 4'b0011 : decoder = 7'b1111001; 4'b0100 : decoder = 7'b0110011; 4'b0101 : decoder = 7'b1011011; 4'b0110 : decoder = 7'b1011111; 4'b0111 : decoder = 7'b1110000; 4'b1000 : decoder = 7'b1111111; 4'b1001 : decoder = 7'b1111011; 4'b1010 : decoder = 7'b1110111; 4'b1011 : decoder = 7'b0011111; 4'b1100 : decoder = 7'b1001110; 4'b1101 : decoder = 7'b0111101; 4'b1110 : decoder = 7'b1001111; 4'b1111 : decoder = 7'b1000111; endcase endfunction endmodule
C#ソースです。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Collections; using System.Runtime.InteropServices; namespace cpu { public partial class Form1 : Form { public Form1() { InitializeComponent(); command_array = new int[10000]; } [DllImport("USER32.DLL")] public static extern int FindWindow( int hWnd, String lpText ); [DllImport("USER32.DLL")] public static extern void PostMessage( int hWnd, int com,int wp,int lp); int wp = 0; int lp = 0; int[] command_array; int a_port_address=0; int b_port_address=0; int write_port_address=0; int current_pc=0; uint clock = 0; uint regfile_R0 = 0; uint regfile_PORT = 0; uint regfile_R1 = 0; uint regfile_a_port=0; uint regfile_b_port=0; uint regfile_write_port=0; uint alu_port_a=0; uint alu_port_b=0; uint alu_out_port=0; uint loop_counter=0xffffffff; uint alu_operation = 0; uint alu_jump_cond = 0; string get_register_name(int num) { switch (num) { case (0): return "ゼロ"; case (1): return "R0"; case (2): return "R1"; case (3): return "ポート"; } return ""; } private void make_code() { int command = (lp >> 28 ) & 0xf; int address = wp - 256; if (address == 0) { checkedListBox1.Items.Clear(); clock = 0; command_array.Initialize(); } if(address>=0) command_array[address] = lp; int a_port_address=(lp >>26) & 3 ; int b_port_address = (lp >> 24) & 3; int write_port_address=(lp>>22) & 3; int jump_cond = (lp >> 19) & 7; int loop_flag = (lp >> 16) & 1; uint immediate =(uint)( lp & 0xffff); string str; str =address.ToString("d4"); str +=" : "; if (lp == 0) { str +="NOP"; checkedListBox1.Items.Add(str); } else if (command == 0)//Add { if (a_port_address == 0 && b_port_address == 0)//CLR { string str_word = str+get_register_name(write_port_address); str_word += "をクリア"; checkedListBox1.Items.Add(str_word); }else if (b_port_address == 0)//MOV { string str_word = str+get_register_name(a_port_address); str_word += "から"; str_word += get_register_name(write_port_address); str_word += "へのコピー"; checkedListBox1.Items.Add(str_word); } else {//Add string str_word =str+ get_register_name(write_port_address); str_word += " <= "; str_word += get_register_name(a_port_address); str_word += " + "; str_word += get_register_name(b_port_address); if (loop_flag == 1) str_word += " ,ループ付"; checkedListBox1.Items.Add(str_word); } } else if (command == 1)//load IM { string str_word = str + get_register_name(write_port_address); str_word += "に"; str_word += immediate.ToString("d"); str_word += "dec , "; str_word += immediate.ToString("X"); str_word += "hexをロード"; checkedListBox1.Items.Add(str_word); } else if (command == 2)//SUB { string str_word = str + get_register_name(write_port_address); str_word += " <= "; str_word += get_register_name(a_port_address); str_word += " - "; str_word += get_register_name(b_port_address); checkedListBox1.Items.Add(str_word); } else if (command == 3)// { string str_word = str + get_register_name(write_port_address); str_word += " <= "; str_word += get_register_name(b_port_address); str_word += " >> 1 "; if (loop_flag == 1) str_word += " ,ループ付"; checkedListBox1.Items.Add(str_word); } else if (command == 4)// { string str_word = str + get_register_name(write_port_address); str_word += " <= "; str_word += get_register_name(b_port_address); str_word += " << 1 "; if (loop_flag == 1) str_word += " ,ループ付"; checkedListBox1.Items.Add(str_word); } else if (command == 5)//JUMP { string str_word = str ; if (jump_cond==2) {//always str_word += immediate.ToString("d"); str_word += "番地にジャンプ"; checkedListBox1.Items.Add(str_word); }else if (jump_cond==3) {//not always str +="NOP"; checkedListBox1.Items.Add(str); }else { str_word += get_register_name(a_port_address); if (jump_cond == 0) str_word += " = "; else if (jump_cond == 1) str_word += " != "; else if (jump_cond == 4) str_word += " < "; else if (jump_cond == 5) str_word += " > "; else if (jump_cond == 6) str_word += " <= "; else if (jump_cond == 7) str_word += " >= "; str_word += get_register_name(b_port_address); str_word += "なら"; str_word += immediate.ToString("d"); str_word += "番地にジャンプ"; checkedListBox1.Items.Add(str_word); } } else if (command == 6)//Sync { string str_word = str ; str_word += "Sync待ち"; checkedListBox1.Items.Add(str_word); } } private void draw_values() { int address = wp - 256; if (address >= 0) make_code(); else { switch (wp) { case (0) :current_pc=lp;break; case (1): regfile_a_port =(uint) lp; break; case (2): regfile_b_port =(uint) lp; break; case (3): regfile_write_port =(uint) lp; break; case (7): alu_port_a = (uint)lp; break; case (8): alu_port_b = (uint)lp; break; case (9): alu_out_port = (uint)lp; break; case (4): regfile_R0 = (uint)lp; break; case (5): regfile_R1 = (uint)lp; break; case (6): regfile_PORT = (uint)lp; break; case (10): alu_operation = (uint)lp; break; case (11): alu_jump_cond = (uint)lp; break; case (12): loop_counter = (uint)lp; break; } } } private void draw_alu(Graphics g,int x, int y) { Pen p = new Pen(Color.Black, 2); Point[] ps = {new Point(10+x, 30+y), new Point(220+x, 30+y), new Point(200+x, 130+y), new Point(30+x, 130+y)}; //折れ線を引く g.DrawPolygon(p, ps); string a_port_str = alu_port_a.ToString("X4"); string b_port_str = alu_port_b.ToString("X4"); string write_port_str = alu_out_port.ToString("X4"); string alu_op_str = "オペレーション"; switch (alu_operation) { case (0): alu_op_str += "Add"; break; case (2): alu_op_str += "Sub"; break; case (3): alu_op_str += " >> "; break; case (4): alu_op_str += " << "; break; case (5): alu_op_str += " Jump"; switch (alu_jump_cond) { case (0): alu_op_str += "if Eq"; break; case (1): alu_op_str += "if Not Eq"; break; case (2): alu_op_str += "Always"; break; case (3): alu_op_str += "Always Not"; break; case (4): alu_op_str += "if < "; break; case (5): alu_op_str += "if > "; break; case (6): alu_op_str += "if <= "; break; case (7): alu_op_str += "if >= "; break; } break; } Font objFont1 = new Font("MS Pゴシック", 11); g.DrawString(a_port_str, objFont1, Brushes.Blue, 50+x, 30+y); g.DrawString(b_port_str, objFont1, Brushes.Blue, 150+x, 30+y); g.DrawString(write_port_str, objFont1, Brushes.Blue, 100+x, 110+y); g.DrawString(alu_op_str, objFont1, Brushes.Blue, 25 + x, 70 + y); } private void draw_regfile(Graphics g, int x, int y) { Pen p = new Pen(Color.Black, 2); Point[] ps = {new Point(10+x, 30+y), new Point(220+x, 30+y), new Point(220+x, 130+y), new Point(10+x, 130+y)}; //折れ線を引く g.DrawPolygon(p, ps); string a_port_str = regfile_a_port.ToString("X4"); string b_port_str = regfile_b_port.ToString("X4"); string write_port_str = regfile_write_port.ToString("X4"); string regfile_zero_str = "・ゼロレジ =0"; string regfile_R0_str = "・R0 ="+regfile_R0.ToString("X4"); string regfile_R1_str = "・R1 ="+regfile_R1.ToString("X4"); string regfile_Port_str = "・ポートレジ="+regfile_PORT.ToString("X4"); Font objFont1 = new Font("MS Pゴシック", 11); g.DrawString(a_port_str, objFont1, Brushes.Blue, 70, 140); g.DrawString(b_port_str, objFont1, Brushes.Blue, 180, 140); g.DrawString(write_port_str, objFont1, Brushes.Blue, 130, 60); g.DrawString(regfile_zero_str, objFont1, Brushes.Green, 50, 75); g.DrawString(regfile_R0_str, objFont1, Brushes.Green, 50, 90); g.DrawString(regfile_R1_str, objFont1, Brushes.Green, 50, 105); g.DrawString(regfile_Port_str, objFont1, Brushes.Green, 50, 120); } private void draw_pc(Graphics g, int x, int y) { Pen p = new Pen(Color.Black, 2); Point[] ps = {new Point(10+x, 10+y), new Point(140+x, 10+y), new Point(140+x, 40+y), new Point(10+x, 40+y)}; //折れ線を引く g.DrawPolygon(p, ps); Font objFont1 = new Font("MS Pゴシック", 12); string str; str = "PC="; str += current_pc.ToString("d2"); g.DrawString(str, objFont1, Brushes.Blue, 10, 20); string lc_str="LC="; lc_str += loop_counter.ToString("x4"); Font objFont2 = new Font("MS Pゴシック", 10); g.DrawString(lc_str, objFont2, Brushes.Blue, 80, 45); if (checkedListBox1.Items.Count > current_pc) { checkedListBox1.SetSelected(current_pc , true); } string clock_str = "Total "+clock.ToString("x4") +"Clocks"; g.DrawString(clock_str, objFont1, Brushes.Brown, 145, 5); } private void paint(object sender, PaintEventArgs e) { Graphics g = e.Graphics; Pen p = new Pen(Color.Black, 2); draw_regfile(g, 30, 30); g.DrawLine(p, 90, 160, 90, 230); g.DrawLine(p, 200, 160, 200, 230); draw_alu(g,30,200); g.DrawLine(p, 150, 330, 150, 360); g.DrawLine(p, 150, 360, 290, 360); g.DrawLine(p, 290, 360, 290, 30); g.DrawLine(p, 290, 30, 150, 30); g.DrawLine(p, 150, 30, 150, 60); } protected override void WndProc(ref Message m) { const int WM_CLOSE = 0x0010; const int WM_ENDSESSION = 0x16; const int WM_SYSCOMMAND = 0x112; const int SC_CLOSE = 0xF060; const int WM_COMMAND = 0x111; const int WM_SIZE = 0x0005; switch (m.Msg) { case WM_ENDSESSION: //OSのシャットダウンで閉じられようとしている //Console.WriteLine("WM_ENDSESSION"); break; case WM_SYSCOMMAND: //if (m.WParam.ToInt32() == SC_CLOSE) //Xボタン、コントロールメニューの「閉じる」、 //コントロールボックスのダブルクリック、 //Atl+F4などにより閉じられようとしている // Console.WriteLine("SC_CLOSE"); break; case WM_CLOSE: //Application.Exit以外で閉じられようとしている //Console.WriteLine("WM_CLOSE"); break; case WM_SIZE: pictureBox1.Refresh(); pictureBox2.Refresh(); break; case WM_COMMAND: wp = (int)m.WParam; lp = (int)m.LParam; clock++; draw_values(); pictureBox1.Refresh();//再ドロー pictureBox2.Refresh(); break; } base.WndProc(ref m);//baseの後に書き換えないとだめ if (m.Msg==WM_COMMAND && wp == 0)//この後$stopするので、このコマンドは最後に送ること { if (checkedListBox1.Items.Count > current_pc) { if (checkedListBox1.GetItemChecked(current_pc))//BreakpointチェックがOnなら { m.Result = (IntPtr)(-1);//Mark As breakpoint } } } } private void paint_picture_box2(object sender, PaintEventArgs e) { Graphics g = e.Graphics; Pen p = new Pen(Color.Black, 2); draw_pc(g, 0, 0); } private void StepButton(object sender, EventArgs e) { const int WM_COMMAND = 0x111; int veritakwin_handle=FindWindow(0,"VeritakWin"); if (veritakwin_handle !=0) {//VeritakWinが存在するなら PostMessage(veritakwin_handle,WM_COMMAND,133,0);//GO Command をVeritakWinに送る } } private void checkedListBox1_double_click(object sender, MouseEventArgs e) { } private void Stop_Button_clicked(object sender, EventArgs e) { const int WM_COMMAND = 0x111; int veritakwin_handle = FindWindow(0, "VeritakWin"); if (veritakwin_handle != 0) {//VeritakWinが存在するなら PostMessage(veritakwin_handle, WM_COMMAND, 134, 0);//GO Command をVeritakWinに送る } } private void Reload_and_go_Button_clicked(object sender, EventArgs e) { const int WM_COMMAND = 0x111; int veritakwin_handle = FindWindow(0, "VeritakWin"); if (veritakwin_handle != 0) {//VeritakWinが存在するなら PostMessage(veritakwin_handle, WM_COMMAND, 153, 0);//GO Command をVeritakWinに送る } } } }
CPU の拡張
さて、一応上の記述で動きますが、やはり自分でいじってみないと良く分からないかもしれません。是非、上を雛形として、
という変更を行ってみてください。以下は、いくつかの拡張案です。
もちろん、MAX2で動かすとなると無理かもしれませんが、シミュレーションを行う分には、自由です。
それでは、実際に、変更を加える例をやってみましょう。
仕様は、次のコマンドを追加するものとします。ifdef で追加しています。
ADDITIONAL_COMMANDがdefineされているときに、Enableされます。このようにしておくと、元に戻したいときに便利です。
なんと、乗算器まで、定義してしまいました。大丈夫でしょうか?
次は、ALUに演算記述を追加します。乗算は、8x8=16ビット符号なしになります。
それから、parameter 定義も追加しておきます。
最後にROM コードです。
入力の値を100倍して、出力ポートに書き出しています。
MAX2での合成結果です。ふぅ、収まりました。
41MHzで走るそうです。
遅延シミュレーションです。
クロックエッジから8ns程度遅れますが、ちゃんと100倍になって返ってきました。
もう、このマジックは、お分かりですね。ハードとしてのROMを解析するとADDや、SUB、その他ロジックは使われていないことを合成器は分かったのでしょう。
これを、純ROMとして定義してしまうと、乗算器は凄く食うので、到底載らないでしょう。
<次の拡張アイデア>
ところで、この乗算器は、JUMPが入っているために、2CYCLEかかってしまっています。ここにループ命令をもってくれば、ループ期間は、1CYCLEで処理できます。
これは、マクロ命令を定義するだけで可能です。或いは、JUMP命令と融合するというアイデアも可能でしょう。命令が終わったら常にジャンプするという具合です。
(IMMEDIATE命令を除き、このフィールドは、遊んでいます)幸い、マイクロフィールドは、未だ2ビット残っていますので、このビットを使って、ビットが立っているときだけ、そういう風にするということもできます。
或いは、このCPUを何個が並列で使うアイデアもありでしょう。割り込みなんて、1サイクルで処理できてしまいます。。
ステートマシンでは、複雑すぎる、CPUでは遅すぎるという場合に好適なハードとソフトの中間みたいなマシンです。こういうハードをマイクロシーケンサと呼んでいます。
昔のCISCマシン(68000の時代)は、マイクロシーケンサで制御されていたと思います。CPUの中のCPUそれが、マイクロシーケンサです。
アーカイブです。
<番外編>
ST2だとどうなるかやってみました。動作速度140Mz、リソース2%の消費でした。
ユーザContribution
ユーザ様から、拡張例をいただきました。
以下は、作者の方のコメントです。
ポート入出力を明示的に分けたり、命令セットや動作仕様を
自分なりにしっくり来るように変更したりしてみました。
ROMのマイクロコードだけ見ていると、アセンブラみたい(笑)
// バレルシフタ
1514: `SET(R0,
2);
1515: `SET(R1, 7);
1516: `SET(R2, 0);
1517: `BSHIFT_L(R0, R1,
R0);
1518: `BSHIFT_R(R0, R1, R0);
でも、実は組み合わせ回路に合成される辺り、何だか、楽しいですね。
調子に乗って、命令とか増やしたり、
ROM出力のビット幅を増やしたりしてみましたが、
クリティカルパスが益々長くなってしまいました。 。
ソースです。
// マイクロプログラム方式 CPU // ネイティブコマンド `define ADD_com 5'b00000 `define SET_com 5'b00001 `define SUB_com 5'b00010 `define SHIFT_L_com 5'b00011 `define SHIFT_R_com 5'b00100 `define JMP_com 5'b00101 `define SYNC_com 5'b00110 `define AND_com 5'b00111 `define OR_com 5'b01000 `define XOR_com 5'b01001 `define NOT_com 5'b01010 `define MUL_com 5'b01011 `define BSHIFT_L_com 5'b01100 `define BSHIFT_R_com 5'b01101 // ジャンプ条件 `define Eq 4'b0000 `define Not_Eq 4'b0001 `define Always 4'b0010 `define LT 4'b0011 `define GT 4'b0100 `define LTE 4'b0101 `define GTE 4'b0110 // レジスタアドレス `define ZR 4'b0000 `define R0 4'b0001 `define R1 4'b0010 `define R2 4'b0011 `define R3 4'b0100 `define P_IN 4'b0101 `define P_OUT 4'b0110 // CPU module cpu(input clock, reset, input [15:0] port_in_data, input sync, output [15:0] port_out_data); localparam integer Loop_counter_width=16; localparam integer Stop_count = 2**Loop_counter_width -1; reg [Loop_counter_width-1:0] loop_counter; reg [15:0] pc; // reg [15:0] port_in_reg; reg sync_ff, sync_ff2; /*---------------------------------------------------------------------------- ROMワイヤーマップ ビット位置 ビット幅 内容 39:35 5 命令 34:31 4 レジスタファイルAポートのアドレス 30:27 4 レジスタファイルBポートのアドレス 26:23 4 レジスタファイルライトポートのアドレス 22:19 4 ジャンプ比較条件 18:16 3 将来拡張用 16:16 1 loop_flag ループ処理するかどうかのフラグ、1でループ処理 15:0 16 即値または、飛び先 -----------------------------------------------------------------------------*/ wire [39:0] rom_data; wire [4:0] command = rom_data[39:35]; wire [3:0] A_address = rom_data[34:31]; wire [3:0] B_address = rom_data[30:27]; wire [3:0] write_address = rom_data[26:23]; wire [3:0] jump_cond = rom_data[22:19]; wire [2:0] reserved = rom_data[18:16]; wire [15:0] immediate_data = rom_data[15:0]; wire loop_flag = reserved[0]; wire [15:0] out_dataA, out_dataB; wire [15:0] alu_data; // SETコマンドの時は、"Aポートに代入する値" を接続する。 // ※ ALUで ゼロレジスタと加算して代入する事で、SETを実現する為。 wire [15:0] Aport_data = (command==`SET_com) ? immediate_data : port_in_data; //ループカウンタリセット条件 wire Reset_loop_counter = (loop_counter<2)|| (loop_counter==Stop_count && immediate_data == 16'b0) ? 1 : 0; //ループカウンタの処理 always @(posedge clock, posedge reset) begin if(reset) begin loop_counter <= Stop_count; end else if(Reset_loop_counter) begin loop_counter <= Stop_count; end else if(loop_flag && loop_counter==Stop_count) begin // ループカウント値をセット loop_counter <= immediate_data; end //else if ((loop_flag) && (loop_counter>1)) else if(loop_flag) begin // ループ処理 loop_counter <= loop_counter - 16'b1; end else begin loop_counter <= Stop_count; end end // 入力同期エッジ検出 always @(posedge clock, posedge reset) begin if (reset) sync_ff<=0; else sync_ff<=sync; end always @(posedge clock, posedge reset) begin if (reset) sync_ff2<=0; else sync_ff2<=sync_ff; end // 立ち上がりエッジを検出 wire sync_pos_edge_detected = sync_ff & ~sync_ff2; // マイクロプログラムカウンタ always @(posedge clock, posedge reset) begin if(reset) begin pc <= 0; end // ジャンプ命令処理 else if(command==`JMP_com && alu_data[0]==1'b1) begin //飛んでいけ pc <= immediate_data; end else if(command==`SYNC_com) begin //同期待ちスタート処理 if(sync_pos_edge_detected) begin //同期信号を検出。次のアドレスを実行開始。 pc <= pc + 1; end else begin //同期エッジが来るまで待つ pc <= pc; end end else if(loop_flag) begin //ループ処理 if(Reset_loop_counter) begin //ループ処理から抜ける pc <= pc + 1; end else begin //ダウンカウント中は待つ pc <= pc; end end else begin // 何もない時は次のアドレスを実行開始 pc <= pc + 1; end end /* //input reg always @(posedge clock, posedge reset) begin if(reset) begin port_in_reg<=0; end else begin port_in_reg<=port_in_data; end end */ // レジスタファイル register_file register_file(.clock(clock), .reset(reset), .A_address(A_address), .B_address(B_address), .write_address(write_address), .alu_data(alu_data), .out_dataA(out_dataA), .out_dataB(out_dataB), .port_in_data(Aport_data), .port_out_data(port_out_data)); // ALU alu alu(.command(command), .jump_cond(jump_cond), .portA(out_dataA), .portB(out_dataB), .alu_out(alu_data)); // ROM rom rom(.rom_address(pc),.Data(rom_data)); endmodule // レジスタファイル // 記憶素子をレジスタの集まりとして、まとめてモジュール化。 // 同時に読み書きできるポートが3つ // ライトポートが1つ、リードポートが2つ。 module register_file(input clock, input reset, input [3:0] A_address, input [3:0] B_address, input [3:0] write_address, input [15:0] alu_data, port_in_data, output [15:0] out_dataA, out_dataB, port_out_data); // CPUレジスタ reg [15:0] p_out_reg, r0, r1, r2, r3; // ポートアウトレジスタ always @(posedge clock, posedge reset) begin if(reset) begin p_out_reg <= 0; end else if(write_address == `P_OUT) begin p_out_reg <= alu_data; end else begin p_out_reg <= p_out_reg; end end // 汎用レジスタR0 always @(posedge clock, posedge reset) begin if(reset) begin r0 <= 0; end else if(write_address == `R0) begin r0 <= alu_data; end else begin r0 <= r0; end end // 汎用レジスタR1 always @(posedge clock, posedge reset) begin if(reset) begin r1 <= 0; end else if(write_address == `R1) begin r1 <= alu_data; end else begin r1 <= r1; end end // 汎用レジスタR2 always @(posedge clock, posedge reset) begin if(reset) begin r2 <= 0; end else if(write_address == `R2) begin r2 <= alu_data; end else begin r2 <= r2; end end // 汎用レジスタR3 always @(posedge clock, posedge reset) begin if(reset) begin r3 <= 0; end else if(write_address == `R3) begin r3 <= alu_data; end else begin r3 <= r3; end end // Aポート出力(ALUへ) assign out_dataA = (A_address==`P_OUT) ? p_out_reg : (A_address==`P_IN) ? port_in_data : (A_address==`R0) ? r0 : (A_address==`R1) ? r1 : (A_address==`R2) ? r2 : (A_address==`R3) ? r3 : 16'b0; // Bポート出力(ALUへ) assign out_dataB = (B_address==`P_OUT) ? p_out_reg : (B_address==`P_IN) ? port_in_data : (B_address==`R0) ? r0 : (B_address==`R1) ? r1 : (B_address==`R2) ? r2 : (B_address==`R3) ? r3 : 16'b0; // 出力ポート(そのまま出力) assign port_out_data = p_out_reg; endmodule // ALU module alu(input [4:0] command, input [3:0] jump_cond, input [15:0] portA,portB, output reg [15:0] alu_out); always @(*) begin case (command) `ADD_com: alu_out=portA+portB; `SUB_com: alu_out=portA-portB; `SHIFT_L_com: alu_out=portB << 1; `SHIFT_R_com: alu_out=portB >> 1; `BSHIFT_L_com:alu_out=portA << portB; `BSHIFT_R_com:alu_out=portA >> portB; `MUL_com: alu_out=portA * portB; `AND_com: alu_out=portA & portB; `OR_com: alu_out=portA | portB; `XOR_com: alu_out=portA ^ portB; `NOT_com: alu_out=~portA; `JMP_com: case (jump_cond) `Eq: alu_out=(portA == portB); `Not_Eq: alu_out=(portA != portB); `Always: alu_out=1; `LT: if (portA < portB) alu_out=1; else alu_out=0; `GT: if (portA > portB) alu_out=1; else alu_out=0; `LTE: if (portA <= portB) alu_out=1; else alu_out=0; `GTE: if (portA >= portB) alu_out=1; else alu_out=0; default: alu_out=0; endcase default: alu_out=portA+portB; endcase end endmodule module rom(input [15:0] rom_address, output [39:0] Data); assign Data = romdata(rom_address); //TAK start wire [4:0] command = Data[39:35]; wire [3:0] A_address = Data[34:31]; wire [3:0] B_address = Data[30:27]; wire [3:0] write_address = Data[26:23]; wire [3:0] jump_cond = Data[22:19]; wire [15:0] im_val=Data[15:0]; wire [2:0] loop_cond=Data[18:16]; //TAK end // コマンド parameter [4:0] Set=`SET_com, Add=`ADD_com, Sub=`SUB_com, Shift_L=`SHIFT_L_com, Shift_R=`SHIFT_R_com, Jmp=`JMP_com, Sync=`SYNC_com, Mul=`MUL_com, And=`AND_com, Or =`OR_com, Xor=`XOR_com, Not=`NOT_com, BShift_L=`BSHIFT_L_com, BShift_R=`BSHIFT_R_com; parameter [4:0] \セット =`SET_com, \加算 =`ADD_com, \減算 =`SUB_com, \左シフト =`SHIFT_L_com, \右シフト =`SHIFT_R_com, \ジャンプ =`JMP_com, \シンク命令 =`SYNC_com, \乗算 =`MUL_com, \& =`AND_com, \OR =`OR_com, \XOR =`XOR_com, \NOT =`NOT_com, \左バレルシフト =`BSHIFT_L_com, \右バレルシフト =`BSHIFT_R_com; // レジスタアドレス parameter [3:0] P_OUT=`P_OUT,//IOポート出力 P_IN=`P_IN, //IOポート入力 R0= `R0, R1= `R1, R2= `R2, R3= `R3, ZR= `ZR; // ゼロレジスタ (= ゼロGND) // ジャンプ条件 parameter [3:0] Eq=`Eq, Not_Eq=`Not_Eq, Always=`Always, LT=`LT, GT=`GT, LTE=`LTE, GTE=`GTE; // ループ処理 parameter [2:0] Reserved = 3'b000, LoopEnabled = 3'b001; // マクロ命令 // NOP `define NOP romdata={Add,ZR,ZR,ZR,Eq,Reserved,16'h 0000} // コピー : r0 → r1 `define MOV(r0,r1) romdata={Add,r0,ZR,r1,Eq,Reserved,16'h 0000} // クリア : r0 = 0 `define CLR(r0) romdata={Add,ZR,ZR,r0,Eq,Reserved,16'h 0000} // 定数代入 : r0 = num `define SET(r0,num) romdata={Set,P_IN,ZR,r0,Eq,Reserved,16'd num} // ジャンプ `define JUMP(num) romdata={Jmp,ZR,ZR,ZR,Always,Reserved,16'd num} // 論理反転 `define NOT(r0,r1) romdata={Not,r0,ZR,r1,Always,Reserved,16'h 0000} // 足し算 : r2 = r0 + r1 `define ADD(r0,r1,r2) romdata={Add,r0,r1,r2,Eq,Reserved,16'h 0000} // 引き算 : r2 = r0 - r1 `define SUB(r0,r1,r2) romdata={Sub,r0,r1,r2,Eq,Reserved,16'h 0000} // 乗算 : r2 = r0 * r1 `define MUL(r0,r1,r2) romdata={Mul,r0,r1,r2,Eq,Reserved,16'h 0000} // 条件文 : if(r0<r1) goto num; `define JUMP_IF_LT(r0,r1,num) romdata={Jmp,r0,r1,ZR,LT,Reserved,16'd num} // 条件文 : if(r0>r1) goto num; `define JUMP_IF_GT(r0,r1,num) romdata={Jmp,r0,r1,ZR,GT,Reserved,16'd num} // 条件文 : if(r0<=r1) goto num; `define JUMP_IF_LTE(r0,r1,num) romdata={Jmp,r0,r1,ZR,LTE,Reserved,16'd num} // 条件文 : if(r0>=r1) goto num; `define JUMP_IF_GTE(r0,r1,num) romdata={Jmp,r0,r1,ZR,GTE,Reserved,16'd num} // 条件文 : if(r0==r1) goto num; `define JUMP_IF_EQ(r0,r1,num) romdata={Jmp,r0,r1,ZR,Eq,Reserved,16'd num} // 1ビットシフト `define SHIFT_L(r0,r1) romdata={Shift_L,ZR,r0,r1,Eq,Reserved,16'h 0000} `define SHIFT_R(r0,r1) romdata={Shift_R,ZR,r0,r1,Eq,Reserved,16'h 0000} // バレルシフト `define BSHIFT_L(r0,r1,r2) romdata={BShift_L,r0,r1,r2,Eq,Reserved,16'h 0000} `define BSHIFT_R(r0,r1,r2) romdata={BShift_R,r0,r1,r2,Eq,Reserved,16'h 0000} // 外部入力・立ち上がりエッジまで待つ `define SYNC romdata={Sync,ZR,ZR,ZR,Eq,Reserved,16'h 0000} // ループ繰り返し命令(JUMP命令分の1クロックを節約) // ex : r2 = r0 + r1 を num+1 回繰り返す。その間、pc は 現アドレスで待機。 `define ADD_LOOP(r0,r1,r2,num) romdata={Add,r0,r1,r2,Eq,LoopEnabled,16'd num} `define SUB_LOOP(r0,r1,r2,num) romdata={Sub,r0,r1,r2,Eq,LoopEnabled,16'd num} `define MUL_LOOP(r0,r1,r2,num) romdata={Mul,r0,r1,r2,Eq,LoopEnabled,16'd num} `define SHIFT_L_LOOP(r0,num) romdata={Shift_L,ZR,r0,r0,Eq,LoopEnabled,16'd num} `define SHIFT_R_LOOP(r0,num) romdata={Shift_R,ZR,r0,r0,Eq,LoopEnabled,16'd num} //Tak start 逆アセンブラ reg [8*40:1] str; function [8*3:1] get_rn(input [3:0] addr); case (addr) P_OUT: get_rn="OUT"; P_IN: get_rn="IN"; //IOポート入力 R0: get_rn="R0"; R1: get_rn="R1"; R2: get_rn="R2"; R3: get_rn="R3"; ZR: get_rn="ZR"; // ゼロレジスタ (= ゼロGND) default: get_rn="???"; endcase endfunction always @* begin case (command) Add: if(loop_cond==0) begin if (A_address==ZR && B_address==ZR & write_address==ZR) $sformat(str,"NOP"); else if (B_address==ZR && A_address==ZR ) $sformat(str,"%sをクリア",get_rn(write_address)); else if (B_address==ZR ) $sformat(str,"%sから%sへコピー",get_rn(A_address),get_rn(write_address)); else $sformat(str,"%s=%s + %s",get_rn(write_address),get_rn(A_address),get_rn(B_address)); end else begin if (A_address==ZR && B_address==ZR & write_address==ZR) $sformat(str,"NOP %dループ付",im_val); else if (B_address==ZR && A_address==ZR ) $sformat(str,"%sをクリア %dループ付",get_rn(write_address),im_val); else if (B_address==ZR ) $sformat(str,"%sから%sへコピー %dループ付",get_rn(A_address),get_rn(write_address),im_val); else $sformat(str,"%s=%s + %s %dループ付",get_rn(write_address),get_rn(A_address),get_rn(B_address),im_val); end Set: $sformat(str,"%s=%h[Hex]",get_rn(write_address),im_val); Mul: if (loop_cond==0) begin $sformat(str,"%s=%s * %s",get_rn(write_address),get_rn(A_address),get_rn(B_address)); end else $sformat(str,"%s=%s * %s %dループ付",get_rn(write_address),get_rn(A_address),get_rn(B_address),im_val); Sub: if (loop_cond==0) begin $sformat(str,"%s=%s - %s",get_rn(write_address),get_rn(A_address),get_rn(B_address)); end else $sformat(str,"%s=%s - %s %dループ付",get_rn(write_address),get_rn(A_address),get_rn(B_address),im_val); Sync: $sformat(str,"シンク命令"); Not: $sformat(str,"%s=~%s",get_rn(write_address),get_rn(A_address)); Jmp: if (jump_cond==Always) $sformat(str,"%d番地へジャンプ",im_val); else if (jump_cond==LT) $sformat(str,"%s < %s なら%d番地へジャンプ",get_rn(A_address),get_rn(B_address),im_val); else if (jump_cond==GT) $sformat(str,"%s > %s なら%d番地へジャンプ",get_rn(A_address),get_rn(B_address),im_val); else if (jump_cond==LTE) $sformat(str,"%s <= %s なら%d番地へジャンプ",get_rn(A_address),get_rn(B_address),im_val); else if (jump_cond==GTE) $sformat(str,"%s >= %s なら%d番地へジャンプ",get_rn(A_address),get_rn(B_address),im_val); else if (jump_cond==Eq) $sformat(str,"%s == %s なら%d番地へジャンプ",get_rn(A_address),get_rn(B_address),im_val); Shift_L: if (loop_cond==0) $sformat(str,"%s=%s << 1",get_rn(write_address),get_rn(B_address)); else $sformat(str,"%s=%s << %d",get_rn(write_address),get_rn(B_address),im_val); Shift_R: if (loop_cond==0) $sformat(str,"%s=%s >> 1",get_rn(write_address),get_rn(B_address)); else $sformat(str,"%s=%s >> %d",get_rn(write_address),get_rn(B_address),im_val); BShift_L: $sformat(str,"%s=%s <<%s",get_rn(write_address),get_rn(A_address),get_rn(B_address)); BShift_R: $sformat(str,"%s=%s >> %s",get_rn(write_address),get_rn(A_address),get_rn(B_address)); endcase end //Tak end // マイクロコード function [39:0]romdata; input [15:0] address; case(address) //Commad A_addr, B_addr, Write_addr, Jmp_cond, Reserved, IM 0: `SET(P_OUT, 100); 1: `SET(R0, 5); 2: `MOV(R0, R1); 3: `MOV(R1, R2); 4: `MOV(R2, R3); 5: `SUB(R1, R2, R0); 6: `SUB(R3, R1, R3); 7: `ADD(P_IN, P_OUT, P_OUT); 8: `ADD(P_OUT, P_OUT, P_OUT); 9: `SUB(P_IN, P_IN, R1); 10: `SUB(P_OUT, P_IN, R2); 11: `SET(R0, 2); 12: `SET(R1, 8); 13: `MUL(R0, R1, R3); 14: `NOP; 15: `MUL(R3, R3, R3); 16: `ADD(P_OUT, R3, P_OUT); 17: `SYNC; 18: `JUMP(1000); // シフト・乗算 1000: `SET(R0, 5); 1001: `SET(R1, 2); 1002: `ADD_LOOP(R0, R1, R0, 10); 1003: `SHIFT_L_LOOP(R1, 5); 1004: `SHIFT_R_LOOP(R1, 5); 1005: `SUB_LOOP(R0, R1, R0, 10); 1006: `SET(P_OUT, 5); 1007: `MUL_LOOP(P_OUT, R1, P_OUT, 7); 1008: `JUMP(1500); // 条件文 1500: `SET(R0, 16); 1501: `SET(R1, 2); 1502: `SET(P_OUT, 0); 1503: `SUB(R0, R1, R0); 1504: `JUMP_IF_GT(R0, R1 ,1503); // 条件文 1505: `SET(R2, 16); 1506: `SET(R3, 2); 1507: `SET(P_OUT, 0); 1509: `SUB(R2, R3, R2); 1511: `JUMP_IF_GTE(R2, R3 ,1509); // 1010_1010_1010_1010 // ビット反転 1512: `SET(R0, 43690); 1513: `NOT(R0, R1); // バレルシフタ 1514: `SET(R0, 2); 1515: `SET(R1, 7); 1516: `SET(R2, 0); 1517: `BSHIFT_L(R0, R1, R0); 1518: `BSHIFT_R(R0, R1, R0); 1519: `JUMP(0); default: romdata = 0; endcase endfunction endmodule
アーカイブです。
なお、記述の動作確認としてQuartasUで合成をしてみました。デバイスはStratix2
速度は、72MHzでした。
内部動作の確認をしたい為に、pcとrom_dataを引き出しています。
`Debug で、切り替えしています。
`define Debug // CPU module cpu(input clock, reset, input [15:0] port_in_data, input sync, output [15:0] port_out_data `ifdef Debug , output [39:0] rom_data, output reg [15:0] pc `endif ); localparam integer Loop_counter_width=16; localparam integer Stop_count = 2**Loop_counter_width -1; reg [Loop_counter_width-1:0] loop_counter; `ifndef Debug reg [15:0] pc; `endif // reg [15:0] port_in_reg; reg sync_ff, sync_ff2; /*---------------------------------------------------------------------------- ROMワイヤーマップ ビット位置 ビット幅 内容 39:35 5 命令 34:31 4 レジスタファイルAポートのアドレス 30:27 4 レジスタファイルBポートのアドレス 26:23 4 レジスタファイルライトポートのアドレス 22:19 4 ジャンプ比較条件 18:16 3 将来拡張用 16:16 1 loop_flag ループ処理するかどうかのフラグ、1でループ処理 15:0 16 即値または、飛び先 -----------------------------------------------------------------------------*/ `ifndef Debug wire [39:0] rom_data; `endif
シミュレーションの最後ところです。青色が遅延SIMで、トップ階層の信号をVCDで記録したものです。
さすがにrom_dataの遅延が大きくなっていますが、RTL SIMと一致しています。
これで、動作の検証は終了です。
このCPUを
spartan3でも合成してみました。速度は40MHzです。
遅延シミュレーションです。StratixUと比べるとやはり遅延は大きくなります。
シンクロナスROMの実装
現在のFPGAのRAM/ROMは、クロックに同期して動作するタイプが主流ですので、今までのRTL SIMからその辺を修正する必要があります。
AlteraのROM作成
Mega−Wizardで生成します。
下のように、address_latachがdefault で付いてしまいます。この対応については後述します。
ビット幅は、40ビット、ワード数は、とりあえず現実的な値として2Kワードを指定します。
後段の出力は、レジスタなしにします。
ROMの初期化は、hexファイルを指定します。(hexファイルは後述)
以上で、Wizard終了です。
さて、問題は、プログラムカウンタPCとRAM内のアドレスラッチ動作がダブっていることです。タダで1CLK遅れてしまうので面白くありません。そこで、ROMに渡すPCは、レジスタ出力ではなく、組み合わせ回路のまま、ROMに渡します。
Xilinx版も同様で、pc_comを渡します。
// ROM `ifdef ALTERA micro_rom rom(.address(pc_com),.q(rom_data),.clock(clock));//ROM にはpcではなくpc_comを渡す形に変更 `else `ifdef XILINX micro_rom_xilinx rom(.addr(pc_com),.dout(rom_data),.clk(clock));//ROM にはpcではなくpc_comを渡す形に変更 `else rom rom(.rom_address(pc_com),.Data(rom_data),.clock(clock));//ROM にはpcではなくpc_comを渡す形に変更 `endif `endif
pc_comは、レジスタpcにする前の組み合わせ回路で、以下のようになります。
ここで、pc_comは、clockに対して同期した信号のみで構成することに注意します。FPGA内部は、非同期リセットのFFを除き、全部同期した信号で構成することが重要です。resetは、非同期なので、clockで、同期化した信号にします。
// マイクロプログラムカウンタ reg reset_sync; always @(clock) reset_sync<=reset;//同期化 pc組み合わせ回路は、同期化したものが必要 //pc 組み合わせ回路 //Altera sync rom は、必ずAddressラッチがついてしまうので、 //pcを渡してしまうのは、1CLK DELAYになってしまう。それを防ぐ //ために次アドレスを組み合わせ回路で渡す。メモリ内ラッチはPCとDuplicate動作する reg [15:0] pc_com; always @* begin if(reset_sync) begin pc_com= 0; end // ジャンプ命令処理 else if(command==`JMP_com && alu_data[0]==1'b1) begin //飛んでいけ pc_com= immediate_data; end else if(command==`SYNC_com) begin //同期待ちスタート処理 if(sync_pos_edge_detected) begin //同期信号を検出。次のアドレスを実行開始。 pc_com= pc + 1; end else begin //同期エッジが来るまで待つ pc_com= pc; end end else if(loop_flag) begin //ループ処理 if(Reset_loop_counter) begin //ループ処理から抜ける pc_com= pc + 1; end else begin //ダウンカウント中は待つ pc_com= pc; end end else begin // 何もない時は次のアドレスを実行開始 pc_com= pc + 1; end end always @(posedge clock, posedge reset) begin if(reset) begin pc <= 0; end else pc<=pc_com; end
このようにすれば、RAM内のアドレスラッチは、PCのように振るまうので、今までのRTL SIMと同じタイミングで動作することになります。
XilinxのROM
coregenで、1ポートROMを生成します。
なにも指定せずに進みます。
ROMの初期化ファイルは、rom.coe(後述)を指定します。
ROM初期化ファイルの生成
Altera、Xilinx共専用の初期化ファイルが必要になります。これから先、いちいち他のツールで変換するのは、面倒なので、Verilog HDLソース中、RTL SIM時に自動生成するようにします。
HEXファイルの生成タスクです。micro_romの内容をINTEL HEXファイル(rom.hex)にするタスクです。
//memory の内容をAltera ROM初期化用に //intel-hexフォーマットに変換する //ファイル->rom.hex として出力する //address 64K word max //zxを含まないこと task make_intel_hex(input [7:0] bytes_per_word, input integer address_max); reg [9*8:1] str; integer fi; reg [15:0] ad; integer bytes; reg [7:0] parity; begin fi=$fopen("rom.hex","w"); for (ad=0;ad <=address_max;ad=ad+1) begin $sformat(str,":%2h%4h00",bytes_per_word,ad); parity=bytes_per_word; parity =parity + ad[8*1 +:8];//チェックサム parity =parity + ad[0 +:8];//チェックサム $fwrite(fi,"%s",str); for (bytes=0;bytes<bytes_per_word;bytes=bytes+1) begin: byte_loop reg [7:0] byte; byte=micro_rom[ad][(bytes_per_word-1-bytes)*8 +:8]; parity=parity+byte;//チェックサム $fwrite(fi,"%2h",byte); end parity=~parity +8'h01;//チェックサム $fwrite(fi,"%2h",parity); $fwrite(fi,"\n"); end $fdisplay(fi,":00000001FF"); $fclose(fi); str="rom.ver"; //$convert_hex2ver("rom.hex",40,str);//チェックVeritak Uniqueファンクション end endtask
micro_romの内容をXILINX Coeファイルにするタスクです。
//xilinx coe file task make_coe_file(input [7:0] bytes_per_word, input integer address_max); integer fi; integer ad; begin fi=$fopen("rom.coe","w"); $fdisplay(fi,"memory_initialization_radix=16;"); $fdisplay(fi,"memory_initialization_vector="); for (ad=0;ad <address_max;ad=ad+1) begin $fdisplay(fi,"%h,",micro_rom[ad]); end $fdisplay(fi,"%h;",micro_rom[address_max]); $fclose(fi); end endtask
以上のタスクをRTL SIM時に生成するようにします。
// マイクロコード `define PHY_ADD_MAX (1024*2-1) //ALTERA ROMの模擬コード reg [39:0] micro_rom [0:`PHY_ADD_MAX]; reg [15:0] address_latch; always @(posedge clock) address_latch<=rom_address; // always @(posedge clock ) begin//q latch 出力付の場合、将来パイプライン化したときに必要になる。 always @* begin Data=micro_rom[address_latch]; end `ifdef RTL_SIM integer i; initial begin for (i=0;i<= `PHY_ADD_MAX;i=i+1) begin micro_rom[i]=romdata(i); end make_intel_hex(5,`PHY_ADD_MAX);//RTL SIM時にINTEL HEXファイルを生成する Altera 用 make_coe_file(5,`PHY_ADD_MAX);//RTL SIM時にXilinx用 Coeファイルを生成する end `endif
以上で、シンクロナスROM実装への変更が終わりました。
XILINX,ALTERAの切り替えは、冒頭のDefineの定義の有無です。RTL シミュレーションの場合は、Veritak ProjectにDefineを入れ込めば指定しなくてもよいのですが、合成する場合は、それぞれ定義を変えます。
今回の全体ソースです。
// マイクロプログラム方式 CPU // ネイティブコマンド `define ADD_com 5'b00000 `define SET_com 5'b00001 `define SUB_com 5'b00010 `define SHIFT_L_com 5'b00011 `define SHIFT_R_com 5'b00100 `define JMP_com 5'b00101 `define SYNC_com 5'b00110 `define AND_com 5'b00111 `define OR_com 5'b01000 `define XOR_com 5'b01001 `define NOT_com 5'b01010 `define MUL_com 5'b01011 `define BSHIFT_L_com 5'b01100 `define BSHIFT_R_com 5'b01101 // ジャンプ条件 `define Eq 4'b0000 `define Not_Eq 4'b0001 `define Always 4'b0010 `define LT 4'b0011 `define GT 4'b0100 `define LTE 4'b0101 `define GTE 4'b0110 // レジスタアドレス `define ZR 4'b0000 `define R0 4'b0001 `define R1 4'b0010 `define R2 4'b0011 `define R3 4'b0100 `define P_IN 4'b0101 `define P_OUT 4'b0110 `define Debug //`define ALTERA //Altera の場合コメントを取りXILINXをコメントアウト //`define XILINX //Xilinxの場合コメントを取りALERAをコメントアウト // CPU module cpu(input clock, reset, input [15:0] port_in_data, input sync, output [15:0] port_out_data `ifdef Debug , output [39:0] rom_data, output reg [15:0] pc `endif ); localparam integer Loop_counter_width=16; localparam integer Stop_count = 2**Loop_counter_width -1; reg [Loop_counter_width-1:0] loop_counter; `ifndef Debug reg [15:0] pc; reg [15:0] pc_com;//組み合わせロジック `endif // reg [15:0] port_in_reg; reg sync_ff, sync_ff2; /*---------------------------------------------------------------------------- ROMワイヤーマップ ビット位置 ビット幅 内容 39:35 5 命令 34:31 4 レジスタファイルAポートのアドレス 30:27 4 レジスタファイルBポートのアドレス 26:23 4 レジスタファイルライトポートのアドレス 22:19 4 ジャンプ比較条件 18:16 3 将来拡張用 16:16 1 loop_flag ループ処理するかどうかのフラグ、1でループ処理 15:0 16 即値または、飛び先 -----------------------------------------------------------------------------*/ `ifndef Debug wire [39:0] rom_data; `endif wire [4:0] command = rom_data[39:35]; wire [3:0] A_address = rom_data[34:31]; wire [3:0] B_address = rom_data[30:27]; wire [3:0] write_address = rom_data[26:23]; wire [3:0] jump_cond = rom_data[22:19]; wire [2:0] reserved = rom_data[18:16]; wire [15:0] immediate_data = rom_data[15:0]; wire loop_flag = reserved[0]; wire [15:0] out_dataA, out_dataB; wire [15:0] alu_data; // SETコマンドの時は、"Aポートに代入する値" を接続する。 // ※ ALUで ゼロレジスタと加算して代入する事で、SETを実現する為。 wire [15:0] Aport_data = (command==`SET_com) ? immediate_data : port_in_data; //ループカウンタリセット条件 wire Reset_loop_counter = (loop_counter<2)|| (loop_counter==Stop_count && immediate_data == 16'b0) ? 1 : 0; //ループカウンタの処理 always @(posedge clock, posedge reset) begin if(reset) begin loop_counter <= Stop_count; end else if(Reset_loop_counter) begin loop_counter <= Stop_count; end else if(loop_flag && loop_counter==Stop_count) begin // ループカウント値をセット loop_counter <= immediate_data; end //else if ((loop_flag) && (loop_counter>1)) else if(loop_flag) begin // ループ処理 loop_counter <= loop_counter - 16'b1; end else begin loop_counter <= Stop_count; end end // 入力同期エッジ検出 always @(posedge clock, posedge reset) begin if (reset) sync_ff<=0; else sync_ff<=sync; end always @(posedge clock, posedge reset) begin if (reset) sync_ff2<=0; else sync_ff2<=sync_ff; end // 立ち上がりエッジを検出 wire sync_pos_edge_detected = sync_ff & ~sync_ff2; // マイクロプログラムカウンタ reg reset_sync; always @(clock) reset_sync<=reset;//同期化 pc組み合わせ回路は、同期化したものが必要 //pc 組み合わせ回路 //Altera sync rom は、必ずAddressラッチがついてしまうので、 //pcを渡してしまうのは、1CLK DELAYになってしまう。それを防ぐ //ために次アドレスを組み合わせ回路で渡す。メモリ内ラッチはPCとDuplicate動作する reg [15:0] pc_com; always @* begin if(reset_sync) begin pc_com= 0; end // ジャンプ命令処理 else if(command==`JMP_com && alu_data[0]==1'b1) begin //飛んでいけ pc_com= immediate_data; end else if(command==`SYNC_com) begin //同期待ちスタート処理 if(sync_pos_edge_detected) begin //同期信号を検出。次のアドレスを実行開始。 pc_com= pc + 1; end else begin //同期エッジが来るまで待つ pc_com= pc; end end else if(loop_flag) begin //ループ処理 if(Reset_loop_counter) begin //ループ処理から抜ける pc_com= pc + 1; end else begin //ダウンカウント中は待つ pc_com= pc; end end else begin // 何もない時は次のアドレスを実行開始 pc_com= pc + 1; end end always @(posedge clock, posedge reset) begin if(reset) begin pc <= 0; end else pc<=pc_com; end // レジスタファイル register_file register_file(.clock(clock), .reset(reset), .A_address(A_address), .B_address(B_address), .write_address(write_address), .alu_data(alu_data), .out_dataA(out_dataA), .out_dataB(out_dataB), .port_in_data(Aport_data), .port_out_data(port_out_data)); // ALU alu alu(.command(command), .jump_cond(jump_cond), .portA(out_dataA), .portB(out_dataB), .alu_out(alu_data)); // ROM `ifdef ALTERA micro_rom rom(.address(pc_com),.q(rom_data),.clock(clock));//ROM にはpcではなくpc_comを渡す形に変更 `else `ifdef XILINX micro_rom_xilinx rom(.addr(pc_com),.dout(rom_data),.clk(clock));//ROM にはpcではなくpc_comを渡す形に変更 `else rom rom(.rom_address(pc_com),.Data(rom_data),.clock(clock));//ROM にはpcではなくpc_comを渡す形に変更 `endif `endif endmodule // レジスタファイル // 記憶素子をレジスタの集まりとして、まとめてモジュール化。 // 同時に読み書きできるポートが3つ // ライトポートが1つ、リードポートが2つ。 module register_file(input clock, input reset, input [3:0] A_address, input [3:0] B_address, input [3:0] write_address, input [15:0] alu_data, port_in_data, output [15:0] out_dataA, out_dataB, port_out_data); // CPUレジスタ reg [15:0] p_out_reg, r0, r1, r2, r3; // ポートアウトレジスタ always @(posedge clock, posedge reset) begin if(reset) begin p_out_reg <= 0; end else if(write_address == `P_OUT) begin p_out_reg <= alu_data; end else begin p_out_reg <= p_out_reg; end end // 汎用レジスタR0 always @(posedge clock, posedge reset) begin if(reset) begin r0 <= 0; end else if(write_address == `R0) begin r0 <= alu_data; end else begin r0 <= r0; end end // 汎用レジスタR1 always @(posedge clock, posedge reset) begin if(reset) begin r1 <= 0; end else if(write_address == `R1) begin r1 <= alu_data; end else begin r1 <= r1; end end // 汎用レジスタR2 always @(posedge clock, posedge reset) begin if(reset) begin r2 <= 0; end else if(write_address == `R2) begin r2 <= alu_data; end else begin r2 <= r2; end end // 汎用レジスタR3 always @(posedge clock, posedge reset) begin if(reset) begin r3 <= 0; end else if(write_address == `R3) begin r3 <= alu_data; end else begin r3 <= r3; end end // Aポート出力(ALUへ) assign out_dataA = (A_address==`P_OUT) ? p_out_reg : (A_address==`P_IN) ? port_in_data : (A_address==`R0) ? r0 : (A_address==`R1) ? r1 : (A_address==`R2) ? r2 : (A_address==`R3) ? r3 : 16'b0; // Bポート出力(ALUへ) assign out_dataB = (B_address==`P_OUT) ? p_out_reg : (B_address==`P_IN) ? port_in_data : (B_address==`R0) ? r0 : (B_address==`R1) ? r1 : (B_address==`R2) ? r2 : (B_address==`R3) ? r3 : 16'b0; // 出力ポート(そのまま出力) assign port_out_data = p_out_reg; endmodule // ALU module alu(input [4:0] command, input [3:0] jump_cond, input [15:0] portA,portB, output reg [15:0] alu_out); always @(*) begin case (command) `ADD_com: alu_out=portA+portB; `SUB_com: alu_out=portA-portB; `SHIFT_L_com: alu_out=portB << 1; `SHIFT_R_com: alu_out=portB >> 1; `BSHIFT_L_com:alu_out=portA << portB; `BSHIFT_R_com:alu_out=portA >> portB; `MUL_com: alu_out=portA * portB; `AND_com: alu_out=portA & portB; `OR_com: alu_out=portA | portB; `XOR_com: alu_out=portA ^ portB; `NOT_com: alu_out=~portA; `JMP_com: case (jump_cond) `Eq: alu_out=(portA == portB); `Not_Eq: alu_out=(portA != portB); `Always: alu_out=1; `LT: if (portA < portB) alu_out=1; else alu_out=0; `GT: if (portA > portB) alu_out=1; else alu_out=0; `LTE: if (portA <= portB) alu_out=1; else alu_out=0; `GTE: if (portA >= portB) alu_out=1; else alu_out=0; default: alu_out=0; endcase default: alu_out=portA+portB; endcase end endmodule module rom(input [15:0] rom_address, input clock,//シンクロナスROMに変更 output reg [39:0] Data); //assign Data = romdata(rom_address);//シンクロナスROMに変更 //TAK start wire [4:0] command = Data[39:35]; wire [3:0] A_address = Data[34:31]; wire [3:0] B_address = Data[30:27]; wire [3:0] write_address = Data[26:23]; wire [3:0] jump_cond = Data[22:19]; wire [15:0] im_val=Data[15:0]; wire [2:0] loop_cond=Data[18:16]; //TAK end // コマンド parameter [4:0] Set=`SET_com, Add=`ADD_com, Sub=`SUB_com, Shift_L=`SHIFT_L_com, Shift_R=`SHIFT_R_com, Jmp=`JMP_com, Sync=`SYNC_com, Mul=`MUL_com, And=`AND_com, Or =`OR_com, Xor=`XOR_com, Not=`NOT_com, BShift_L=`BSHIFT_L_com, BShift_R=`BSHIFT_R_com; parameter [4:0] \セット =`SET_com, \加算 =`ADD_com, \減算 =`SUB_com, \左シフト =`SHIFT_L_com, \右シフト =`SHIFT_R_com, \ジャンプ =`JMP_com, \シンク命令 =`SYNC_com, \乗算 =`MUL_com, \& =`AND_com, \OR =`OR_com, \XOR =`XOR_com, \NOT =`NOT_com, \左バレルシフト =`BSHIFT_L_com, \右バレルシフト =`BSHIFT_R_com; // レジスタアドレス parameter [3:0] P_OUT=`P_OUT,//IOポート出力 P_IN=`P_IN, //IOポート入力 R0= `R0, R1= `R1, R2= `R2, R3= `R3, ZR= `ZR; // ゼロレジスタ (= ゼロGND) // ジャンプ条件 parameter [3:0] Eq=`Eq, Not_Eq=`Not_Eq, Always=`Always, LT=`LT, GT=`GT, LTE=`LTE, GTE=`GTE; // ループ処理 parameter [2:0] Reserved = 3'b000, LoopEnabled = 3'b001; // マクロ命令 // NOP `define NOP romdata={Add,ZR,ZR,ZR,Eq,Reserved,16'h 0000} // コピー : r0 → r1 `define MOV(r0,r1) romdata={Add,r0,ZR,r1,Eq,Reserved,16'h 0000} // クリア : r0 = 0 `define CLR(r0) romdata={Add,ZR,ZR,r0,Eq,Reserved,16'h 0000} // 定数代入 : r0 = num `define SET(r0,num) romdata={Set,P_IN,ZR,r0,Eq,Reserved,16'd num} // ジャンプ `define JUMP(num) romdata={Jmp,ZR,ZR,ZR,Always,Reserved,16'd num} // 論理反転 `define NOT(r0,r1) romdata={Not,r0,ZR,r1,Always,Reserved,16'h 0000} // 足し算 : r2 = r0 + r1 `define ADD(r0,r1,r2) romdata={Add,r0,r1,r2,Eq,Reserved,16'h 0000} // 引き算 : r2 = r0 - r1 `define SUB(r0,r1,r2) romdata={Sub,r0,r1,r2,Eq,Reserved,16'h 0000} // 乗算 : r2 = r0 * r1 `define MUL(r0,r1,r2) romdata={Mul,r0,r1,r2,Eq,Reserved,16'h 0000} // 条件文 : if(r0<r1) goto num; `define JUMP_IF_LT(r0,r1,num) romdata={Jmp,r0,r1,ZR,LT,Reserved,16'd num} // 条件文 : if(r0>r1) goto num; `define JUMP_IF_GT(r0,r1,num) romdata={Jmp,r0,r1,ZR,GT,Reserved,16'd num} // 条件文 : if(r0<=r1) goto num; `define JUMP_IF_LTE(r0,r1,num) romdata={Jmp,r0,r1,ZR,LTE,Reserved,16'd num} // 条件文 : if(r0>=r1) goto num; `define JUMP_IF_GTE(r0,r1,num) romdata={Jmp,r0,r1,ZR,GTE,Reserved,16'd num} // 条件文 : if(r0==r1) goto num; `define JUMP_IF_EQ(r0,r1,num) romdata={Jmp,r0,r1,ZR,Eq,Reserved,16'd num} // 1ビットシフト `define SHIFT_L(r0,r1) romdata={Shift_L,ZR,r0,r1,Eq,Reserved,16'h 0000} `define SHIFT_R(r0,r1) romdata={Shift_R,ZR,r0,r1,Eq,Reserved,16'h 0000} // バレルシフト `define BSHIFT_L(r0,r1,r2) romdata={BShift_L,r0,r1,r2,Eq,Reserved,16'h 0000} `define BSHIFT_R(r0,r1,r2) romdata={BShift_R,r0,r1,r2,Eq,Reserved,16'h 0000} // 外部入力・立ち上がりエッジまで待つ `define SYNC romdata={Sync,ZR,ZR,ZR,Eq,Reserved,16'h 0000} // ループ繰り返し命令(JUMP命令分の1クロックを節約) // ex : r2 = r0 + r1 を num+1 回繰り返す。その間、pc は 現アドレスで待機。 `define ADD_LOOP(r0,r1,r2,num) romdata={Add,r0,r1,r2,Eq,LoopEnabled,16'd num} `define SUB_LOOP(r0,r1,r2,num) romdata={Sub,r0,r1,r2,Eq,LoopEnabled,16'd num} `define MUL_LOOP(r0,r1,r2,num) romdata={Mul,r0,r1,r2,Eq,LoopEnabled,16'd num} `define SHIFT_L_LOOP(r0,num) romdata={Shift_L,ZR,r0,r0,Eq,LoopEnabled,16'd num} `define SHIFT_R_LOOP(r0,num) romdata={Shift_R,ZR,r0,r0,Eq,LoopEnabled,16'd num} //Tak start 逆アセンブラ reg [8*40:1] str; function [8*3:1] get_rn(input [3:0] addr); case (addr) P_OUT: get_rn="OUT"; P_IN: get_rn="IN"; //IOポート入力 R0: get_rn="R0"; R1: get_rn="R1"; R2: get_rn="R2"; R3: get_rn="R3"; ZR: get_rn="ZR"; // ゼロレジスタ (= ゼロGND) default: get_rn="???"; endcase endfunction always @* begin case (command) Add: if(loop_cond==0) begin if (A_address==ZR && B_address==ZR & write_address==ZR) $sformat(str,"NOP"); else if (B_address==ZR && A_address==ZR ) $sformat(str,"%sをクリア",get_rn(write_address)); else if (B_address==ZR ) $sformat(str,"%sから%sへコピー",get_rn(A_address),get_rn(write_address)); else $sformat(str,"%s=%s + %s",get_rn(write_address),get_rn(A_address),get_rn(B_address)); end else begin if (A_address==ZR && B_address==ZR & write_address==ZR) $sformat(str,"NOP %dループ付",im_val); else if (B_address==ZR && A_address==ZR ) $sformat(str,"%sをクリア %dループ付",get_rn(write_address),im_val); else if (B_address==ZR ) $sformat(str,"%sから%sへコピー %dループ付",get_rn(A_address),get_rn(write_address),im_val); else $sformat(str,"%s=%s + %s %dループ付",get_rn(write_address),get_rn(A_address),get_rn(B_address),im_val); end Set: $sformat(str,"%s=%h[Hex]",get_rn(write_address),im_val); Mul: if (loop_cond==0) begin $sformat(str,"%s=%s * %s",get_rn(write_address),get_rn(A_address),get_rn(B_address)); end else $sformat(str,"%s=%s * %s %dループ付",get_rn(write_address),get_rn(A_address),get_rn(B_address),im_val); Sub: if (loop_cond==0) begin $sformat(str,"%s=%s - %s",get_rn(write_address),get_rn(A_address),get_rn(B_address)); end else $sformat(str,"%s=%s - %s %dループ付",get_rn(write_address),get_rn(A_address),get_rn(B_address),im_val); Sync: $sformat(str,"シンク命令"); Not: $sformat(str,"%s=~%s",get_rn(write_address),get_rn(A_address)); Jmp: if (jump_cond==Always) $sformat(str,"%d番地へジャンプ",im_val); else if (jump_cond==LT) $sformat(str,"%s < %s なら%d番地へジャンプ",get_rn(A_address),get_rn(B_address),im_val); else if (jump_cond==GT) $sformat(str,"%s > %s なら%d番地へジャンプ",get_rn(A_address),get_rn(B_address),im_val); else if (jump_cond==LTE) $sformat(str,"%s <= %s なら%d番地へジャンプ",get_rn(A_address),get_rn(B_address),im_val); else if (jump_cond==GTE) $sformat(str,"%s >= %s なら%d番地へジャンプ",get_rn(A_address),get_rn(B_address),im_val); else if (jump_cond==Eq) $sformat(str,"%s == %s なら%d番地へジャンプ",get_rn(A_address),get_rn(B_address),im_val); Shift_L: if (loop_cond==0) $sformat(str,"%s=%s << 1",get_rn(write_address),get_rn(B_address)); else $sformat(str,"%s=%s << %d",get_rn(write_address),get_rn(B_address),im_val); Shift_R: if (loop_cond==0) $sformat(str,"%s=%s >> 1",get_rn(write_address),get_rn(B_address)); else $sformat(str,"%s=%s >> %d",get_rn(write_address),get_rn(B_address),im_val); BShift_L: $sformat(str,"%s=%s <<%s",get_rn(write_address),get_rn(A_address),get_rn(B_address)); BShift_R: $sformat(str,"%s=%s >> %s",get_rn(write_address),get_rn(A_address),get_rn(B_address)); endcase end //Tak end // マイクロコード `define PHY_ADD_MAX (1024*2-1) //ALTERA ROMの模擬コード reg [39:0] micro_rom [0:`PHY_ADD_MAX]; reg [15:0] address_latch; always @(posedge clock) address_latch<=rom_address; // always @(posedge clock ) begin//q latch 出力付の場合、将来パイプライン化したときに必要になる。 always @* begin Data=micro_rom[address_latch]; end `ifdef RTL_SIM integer i; initial begin for (i=0;i<= `PHY_ADD_MAX;i=i+1) begin micro_rom[i]=romdata(i); end make_intel_hex(5,`PHY_ADD_MAX);//RTL SIM時にINTEL HEXファイルを生成する Altera 用 make_coe_file(5,`PHY_ADD_MAX);//RTL SIM時にXilinx用 Coeファイルを生成する end `endif //memory の内容をAltera ROM初期化用に //intel-hexフォーマットに変換する //ファイル->rom.hex として出力する //address 64K word max //zxを含まないこと task make_intel_hex(input [7:0] bytes_per_word, input integer address_max); reg [9*8:1] str; integer fi; reg [15:0] ad; integer bytes; reg [7:0] parity; begin fi=$fopen("rom.hex","w"); for (ad=0;ad <=address_max;ad=ad+1) begin $sformat(str,":%2h%4h00",bytes_per_word,ad); parity=bytes_per_word; parity =parity + ad[8*1 +:8];//チェックサム parity =parity + ad[0 +:8];//チェックサム $fwrite(fi,"%s",str); for (bytes=0;bytes<bytes_per_word;bytes=bytes+1) begin: byte_loop reg [7:0] byte; byte=micro_rom[ad][(bytes_per_word-1-bytes)*8 +:8]; parity=parity+byte;//チェックサム $fwrite(fi,"%2h",byte); end parity=~parity +8'h01;//チェックサム $fwrite(fi,"%2h",parity); $fwrite(fi,"\n"); end $fdisplay(fi,":00000001FF"); $fclose(fi); str="rom.ver"; //$convert_hex2ver("rom.hex",40,str);//チェックVeritak Uniqueファンクション end endtask //xilinx coe file task make_coe_file(input [7:0] bytes_per_word, input integer address_max); integer fi; integer ad; begin fi=$fopen("rom.coe","w"); $fdisplay(fi,"memory_initialization_radix=16;"); $fdisplay(fi,"memory_initialization_vector="); for (ad=0;ad <address_max;ad=ad+1) begin $fdisplay(fi,"%h,",micro_rom[ad]); end $fdisplay(fi,"%h;",micro_rom[address_max]); $fclose(fi); end endtask function [39:0]romdata; input [15:0] address; case(address) //Commad A_addr, B_addr, Write_addr, Jmp_cond, Reserved, IM 0: `SET(P_OUT, 100); 1: `SET(R0, 5); 2: `MOV(R0, R1); 3: `MOV(R1, R2); 4: `MOV(R2, R3); 5: `SUB(R1, R2, R0); 6: `SUB(R3, R1, R3); 7: `ADD(P_IN, P_OUT, P_OUT); 8: `ADD(P_OUT, P_OUT, P_OUT); 9: `SUB(P_IN, P_IN, R1); 10: `SUB(P_OUT, P_IN, R2); 11: `SET(R0, 2); 12: `SET(R1, 8); 13: `MUL(R0, R1, R3); 14: `NOP; 15: `MUL(R3, R3, R3); 16: `ADD(P_OUT, R3, P_OUT); 17: `SYNC; 18: `JUMP(1000); // シフト・乗算 1000: `SET(R0, 5); 1001: `SET(R1, 2); 1002: `ADD_LOOP(R0, R1, R0, 10); 1003: `SHIFT_L_LOOP(R1, 5); 1004: `SHIFT_R_LOOP(R1, 5); 1005: `SUB_LOOP(R0, R1, R0, 10); 1006: `SET(P_OUT, 5); 1007: `MUL_LOOP(P_OUT, R1, P_OUT, 7); 1008: `JUMP(1500); // 条件文 1500: `SET(R0, 16); 1501: `SET(R1, 2); 1502: `SET(P_OUT, 0); 1503: `SUB(R0, R1, R0); 1504: `JUMP_IF_GT(R0, R1 ,1503); 1505: `NOP ;//NOP挿入 ディレイドスロット // 条件文 1506: `SET(R2, 16); 1507: `SET(R3, 2); 1508: `SET(P_OUT, 0); 1509: `SUB(R2, R3, R2); 1511: `JUMP_IF_GTE(R2, R3 ,1509); 1512:`NOP;//NOP挿入 ディレイドスロット // 1010_1010_1010_1010 // ビット反転 1513: `SET(R0, 43690); 1514: `NOT(R0, R1); // バレルシフタ 1515: `SET(R0, 2); 1516: `SET(R1, 7); 1517: `SET(R2, 0); 1518: `BSHIFT_L(R0, R1, R0); 1519: `BSHIFT_R(R0, R1, R0); 1520: `JUMP(0); default: romdata = 0; endcase endfunction endmodule
なお、今回の変更で、
StaratixUは、88MHz
Spartan 3は、44MHz となりました。
合成後の遅延シミュレーション結果(青色VCD)です。
Spartan3
同様にStaratixUです。
シンクロナスROMのアーカイブです。
RAMの実装
FPGAのRAMをデータメモリとして使うことを考えます。ここでは、外部RAMを使うことは考慮せずに、FPGA内のRAMを使うことを考えます。
さてここでの問題は、同期RAMにあります。とりあえずWIZARDでRAMを作ってみましょう。
Single_PortRAMを生成します。
とりあえず、16ビット幅2Kワードです。
Q出力なしにします。このダイアグラムの通り、RAMにおいても前段にFFが入ってしまいます。従って、1CLOCK遅れてしまうことに注意してください。CPUから見てWRITEは、書きっぱなしなので1CLOCKでWRITEできますが、READは、1CLOCKでは返ってきません。ここでの設計は大きな分かれ目です。2CLOCKを許容できないとするか、見た目に1CLOCK R/Wとなるように、全体のブロックを考えなおすかという問題になります。後者は、CPU全体のパイプライン化によって達成することができ、パイプラインにより、動作速度を向上させることができます。しかし良い事ばかりではなく、しわ寄せは、JUMP命令に現れます。JUMPアドレスが静的に決まっているならば、先読みにより回避することができますが。JUMPアドレスが動作時にしか決まらない場合、即ち、条件ブランチや、条件CALLがある場合、パイプライン分の段数に応じたクロック数がかかってしまいます。 (1CLOCKでREAD/WRITEするパイプラインの例は、こちらのパイプラインの検討を見てください。パイプラインが深いとそれだけペナルティも大きくなります。)
今回は、簡単な設計を目指しているので、パイプラインは使わず、READのみ2CLOCK、その他は1CLOCKとする方針で設計します。
アドレシングモード
下記の二つを備えます。R0をインデックスとしたアドレシングです。
以下追加部です。
Altera/Xilinx のRAMを入れ込む前に模擬バージョンで動作確認しておきます。
//ALTERA RAMの模擬コード `define PHY_ADD_MAX_RAM (1024*2-1)//Dec.17.2006 reg [15:0] ram [0:`PHY_ADD_MAX_RAM]; reg [15:0] ram_address_latch; reg ram_write_enable_latch; reg [15:0] ram_data_latch,ram_output_data; //アドレスラッチ、データラッチ、ライトEnableラッチがある always @(posedge clock) ram_address_latch<=ram_address; always @(posedge clock) ram_write_enable_latch<=ram_write_enable; always @(posedge clock) ram_data_latch<=ram_input_port; always @* begin ram_output_data=ram[ram_address_latch]; end assign ram_output_port=ram_output_data; always @(negedge clock) begin//立下りで内部RAMに書き込み if (ram_write_enable_latch) begin ram[ram_address_latch] <=ram_data_latch; end end
RAMをレジスタと同じように扱うので、レジスタアドレスのアドレス空間に追加します。
// レジスタアドレス `define ZR 4'b0000 `define R0 4'b0001 `define R1 4'b0010 `define R2 4'b0011 `define R3 4'b0100 `define P_IN 4'b0101 `define P_OUT 4'b0110 `define RAM_PORT 4'b0111 //Dec.17.2006
ROMのビットフィールドにも追加します。
ビット18は、RAMのWriteEnableにします。
ビット17は、インデックスアドレシングか絶対アドレスかを決めているフラグです。
ROMワイヤーマップ ビット位置 ビット幅 内容 39:35 5 命令 34:31 4 レジスタファイルAポートのアドレス 30:27 4 レジスタファイルBポートのアドレス 26:23 4 レジスタファイルライトポートのアドレス 22:19 4 ジャンプ比較条件 18:18 1 RAM_WRITE_Enable //Dec.17.2006 17:17 1 RAM アクセスの場合のR0をindex reg として使用するかどうかのフラグ.1で間接アドレシング//Dec.17.2006 16:16 1 loop_flag ループ処理するかどうかのフラグ、1でループ処理 15:0 16 即値または、飛び先,または、RAMアドレス
RAMのアドレス生成部と、RAMのデータ入力部です。RAMのデータ入力は、ALUの出力をそのまま出します。この辺は、CISCぽいですね。
wire index_flag =reserved[1];//Dec.17.2006 wire [15:0] out_dataA, out_dataB; wire [15:0] alu_data; wire ram_write_enable = reserved[2];//Dec.17.2006 wire [15:0] ram_output_port;//Dec.17.2006 wire [15:0] ram_input_port=alu_data;//Dec.17.2006 wire [15:0] index_reg;//Dec.17.2006 wire [15:0] ram_address=index_flag? immediate_data+index_reg :immediate_data;//Dec.17.2006
レジスタファイルからは、R0をindex_regとして引き出します。
// レジスタファイル register_file register_file(.clock(clock), .reset(reset), .A_address(A_address), .B_address(B_address), .write_address(write_address), .alu_data(alu_data), .out_dataA(out_dataA), .out_dataB(out_dataB), .port_in_data(Aport_data),.ram_port(ram_output_port),.r0(index_reg),//Dec.17.2006 .port_out_data(port_out_data));
ifdef でRAMのインスタンスは、切り替えを行います。
`ifdef ALTERA altera_ram2kx16 altera_ram ( .address(ram_address), .clock(clock), .data(ram_input_port), .wren(ram_write_enable), .q(ram_output_port) ); `elsif XILINX ram_xilinx2k16 xilinx_ram( .addr(ram_address), .clk(clock), .din(ram_input_port), .dout(ram_output_port), .we(ram_write_enable)); `else //ALTERA RAMの模擬コード `define PHY_ADD_MAX_RAM (1024*2-1)//Dec.17.2006 reg [15:0] ram [0:`PHY_ADD_MAX_RAM]; reg [15:0] ram_address_latch; reg ram_write_enable_latch; reg [15:0] ram_data_latch,ram_output_data; //アドレスラッチ、データラッチ、ライトEnableラッチがある always @(posedge clock) ram_address_latch<=ram_address; always @(posedge clock) ram_write_enable_latch<=ram_write_enable; always @(posedge clock) ram_data_latch<=ram_input_port; always @* begin ram_output_data=ram[ram_address_latch]; end assign ram_output_port=ram_output_data; always @(negedge clock) begin//立下りで内部RAMに書き込み if (ram_write_enable_latch) begin ram[ram_address_latch] <=ram_data_latch; end end `endif
レジスタファイルの記述です。
// レジスタファイル // 記憶素子をレジスタの集まりとして、まとめてモジュール化。 // 同時に読み書きできるポートが3つ // ライトポートが1つ、リードポートが2つ。 // R0をIndexRegとして使用する module register_file(input clock, input reset, input [3:0] A_address, input [3:0] B_address, input [3:0] write_address, input [15:0] alu_data, port_in_data,ram_port, //Dec.17.2006 output [15:0] out_dataA, out_dataB, port_out_data, output reg [15:0] r0 ); // CPUレジスタ reg [15:0] p_out_reg, r1, r2, r3; // ポートアウトレジスタ always @(posedge clock, posedge reset) begin if(reset) begin p_out_reg <= 0; end else if(write_address == `P_OUT) begin p_out_reg <= alu_data; end else begin p_out_reg <= p_out_reg; end end // 汎用レジスタR0 always @(posedge clock, posedge reset) begin if(reset) begin r0 <= 0; end else if(write_address == `R0) begin r0 <= alu_data; end else begin r0 <= r0; end end // 汎用レジスタR1 always @(posedge clock, posedge reset) begin if(reset) begin r1 <= 0; end else if(write_address == `R1) begin r1 <= alu_data; end else begin r1 <= r1; end end // 汎用レジスタR2 always @(posedge clock, posedge reset) begin if(reset) begin r2 <= 0; end else if(write_address == `R2) begin r2 <= alu_data; end else begin r2 <= r2; end end // 汎用レジスタR3 always @(posedge clock, posedge reset) begin if(reset) begin r3 <= 0; end else if(write_address == `R3) begin r3 <= alu_data; end else begin r3 <= r3; end end // Aポート出力(ALUへ) assign out_dataA = (A_address==`P_OUT) ? p_out_reg : (A_address==`P_IN) ? port_in_data : (A_address==`RAM_PORT) ? ram_port: //Dec.17.2006 (A_address==`R0) ? r0 : (A_address==`R1) ? r1 : (A_address==`R2) ? r2 : (A_address==`R3) ? r3 : 16'b0; // Bポート出力(ALUへ) assign out_dataB = (B_address==`P_OUT) ? p_out_reg : (B_address==`P_IN) ? port_in_data : (B_address==`RAM_PORT) ? ram_port: //Dec.17.2006 (B_address==`R0) ? r0 : (B_address==`R1) ? r1 : (B_address==`R2) ? r2 : (B_address==`R3) ? r3 : 16'b0; // 出力ポート(そのまま出力) assign port_out_data = p_out_reg; endmodule
RAMのR/Wに関するマイクロ命令の追加部です。
CPU->RAMへの書き込みは、2CLKかかるのですが、CPUからは見えません。ですので1CLKで次の命令を実行できます。
RAM->CPUの読み込みは、2CLKかかるので、二つの命令で構成します。最初の命令は、アドレス指定だけです。次の命令で、読み込み、つまりレジスタへの書き込みを行います。このとき、マイクロフィールドのアドレス部は遊んでいますので、次の読み込みアドレスを指定できます。つまりパイプライン的な記述が可能で、読み込み命令が連続する場合は、1CLKで済みます。
wire indirect_flag=Data[17];//Dec.17.2006 wire ram_write=Data[18];//Dec.17.2006 // レジスタアドレス parameter [3:0] P_OUT=`P_OUT,//IOポート出力 P_IN=`P_IN, //IOポート入力 RAM_PORT=`RAM_PORT, //RAM ポート Dec.17.2006 R0= `R0, R1= `R1, R2= `R2, R3= `R3, ZR= `ZR; // ゼロレジスタ (= ゼロGND) // ループ処理 または、RAMライト処理 //ループ値はRAMアドレスと共用なのでRAMに関しループは不可 parameter [2:0] Reserved = 3'b000, LoopEnabled = 3'b001, RAM_write_enable=3'b100, //Dec.17.2006 ビット2をRAM WriteEnableに使用 RAM_read_port_enable_with_index=3'b011,//Dec.17.2006 ビット1ー> INDEX付き RAM_write_enable_with_index=3'b111; //Dec.17.2006 ビット2をRAM WriteEnableに使用ビット1−>INDEX付き //RAMへライト RAM[address]=source_reg Dec.17.2006 ライトは2クロックかかるがCPUからは見えない `define MOV_TO_RAM(source_reg,address) romdata={Add,source_reg,ZR,ZR,Eq,RAM_write_enable,16'd address} // RAM[address+r0]=source_reg `define MOV_TO_RAM_W_I(source_reg,address) romdata={Add,source_reg,ZR,ZR,Eq,RAM_write_enable_with_index,16'd address} //RAMからリード Dec.17.2006 //READ は2クロックかかるので、2マイクロ命令組で使う 1クロック目でアドレス指定、2クロック目でレジスタへのライト // dest_reg=RAM[address] `define MOV_FROM_RAM_pre(address) romdata={Add,ZR,ZR,ZR,Eq,Reserved,16'd address}//アドレス指定,この間他のレジスタ演算を行っても良い `define MOV_FROM_RAM(dest_reg,address) romdata={Add,RAM_PORT,ZR,dest_reg,Eq,Reserved,16'd address}//レジスタへ書き込み //dest_reg=RAM[address+r0] `define MOV_FROM_RAM_W_I_pre(address) romdata={Add,ZR,ZR,ZR,Eq,RAM_read_port_enable_with_index,16'd address}//アドレス指定,この間他のレジスタ演算を行っても良い `define MOV_FROM_RAM_W_I(dest_reg,address) romdata={Add,RAM_PORT,ZR,dest_reg,Eq,RAM_read_port_enable_with_index,16'd address}//レジスタへ書き込み
マイクロコードの例です。単発読み出しでは、xx_pre命令でアドレス指定した後に読み出す命令が必要ですが、連続読み出しでは、xx_pre命令が不要になっています。
function [39:0]romdata; input [15:0] address; case(address) //Commad A_addr, B_addr, Write_addr, Jmp_cond, Reserved, IM //simple test 0:`SET(R0,1000); 1:`SET(R1,2000); 2:`SET(R2,3000); 3:`MOV_TO_RAM(R0,0); 4:`MOV_TO_RAM(R1,1); 5:`MOV_TO_RAM(R2,2); 6:`MOV_FROM_RAM_pre(0); 7:`MOV_FROM_RAM(P_OUT,0); 8:`MOV_FROM_RAM_pre(1); 9:`MOV_FROM_RAM(P_OUT,1); 10:`MOV_FROM_RAM_pre(2); 11:`MOV_FROM_RAM(P_OUT,2); //index register write test 12:`SET(R3,2048);//for (r0=0;r0<2048;r0++){ 13:`SET(R1,1);//r1=1; 14:`SET(R0,0);//r0=0; 15:`MOV_TO_RAM_W_I(R0,0);//mem[r0+0]=r0 16:`ADD(R0,R1,R0);// r0++; 17:`JUMP_IF_LT(R0, R3 ,15);// } //index register read test 18:`SET(R0,0);//r0=0; 19:`SET(R3,1000); 20:`SET(R1,4); 21:`MOV_FROM_RAM_W_I_pre(1048);//ram_address=r0+1048 22:`MOV_FROM_RAM_W_I(P_OUT,1049);//P_OUT=mem[ram_address] ram_address2=r0+1049 23:`MOV_FROM_RAM_W_I(P_OUT,1050);//P_OUT=mem[ram_addres2] 24:`MOV_FROM_RAM_W_I(P_OUT,1051);//.. software pipeline 25:`MOV_FROM_RAM_W_I(P_OUT,1052);//.. software pipeline 26:`ADD(R0,R1,R0);// r0 +=4; 27:`JUMP_IF_LT(R0, R3 ,21);// } 45:`JUMP(0); ..
RTL Sim
RAM Writeの様子です。1CLK毎に書けます。
Readの様子です。
XILINX RTL SIMと遅延SIM
遅延SIMは、PortOUTの出力をVCDで見ています。RTLと一致しています。
Altera RTL SIMと遅延SIM
同様にAltera Versionです。
RAM版のダウンロード(Generic,Altera,Xilinx、RTL/遅延SIM)
<次の拡張案>
とりあえずRAMの実装は終わったのですが、普通のCPUでは、やっぱりあまり面白くないので、アーキテクチャとしてDSPを志向してみることにしました。FIRフィルタ、FFT,ダウンコンバータ、サーボ演算等で遊べたらよいな?と思います。リソース使用量はあまり気にしないで、できるだけ簡単な設計を心がけたいと思います。コンパイラ作成では、日本ではあまりなじみがないと思われるAntlrというツールを紹介する予定です。構文解析は、Flex/Bisonや、Boost::spiritがよく知られていますが、Antlrもそのようなツールです。このツールは、C++や、JAVA,Pythonを出力します。
DSPライクなアーキテクチャの検討
こういう石にしたい、というのを思いつくままに列挙してみました。
最近のFPGAのアーキテクチャをうまく生かすには?
プロセッサを高速化するには、垂直方向の高速化、すなわちDeep Pipelineが有効です。動的なジャンプがないとしたら、いくらでもパイプライン化で高速にすることができます。問題はジャンプであるということは、上で見ました。ところで、FFTや、FIRは、FOR LOOPによる演算になりますが、これらは、ジャンプ箇所とループ数が実行時に変化することはないので、プログラムによるHW化をすることができます。つまりDeep Pipelineを維持しながら、演算ループではJUMPペナルティが全くない処理をすることは可能です。ただ、予測分岐とか面倒なことはしないので、ジャンプ命令自体は4CLK位Delayed Slot(ジャンプ命令の後も実行してしまう)が発生してもよし、とします。また、最近のFPGAは、乗算器を、数個以上積んでいるのが普通で、遊んでいるのも勿体ない話です。これには水平方向の並列演算が有効でしょう。FPGAでは、シングルALUにこだわる理由はありません。
しかしながら、思いついた仕様は、ハード的に飛躍し過ぎるので、
テーマは、「Verilogでお手軽CPU」に戻ります。
まずは、普通にTINY Cコンパイラが走るCPUを作ることを目標にします。
そこで、普通のCPUの仕様を考えてみます。
テーマが紆余曲折していますが、事情があり、YACCベースでの32ビットCPUハード製作に変更します。TINY Cコンパイラの製作はハード製作後に行います。ごめんなさい。旧YACCとの違いは、
第一に
第二の目標は
です。YACCは、ドライストーンを良く見せることを第一義に設計したために、駆動周波数は速いのですが、外部RAMには接続できず汎用的とは言えません。そこで、多少速度を犠牲にして汎用的な設計を行ってみます。
最終目標は、自前Cコンパイラを使い、OS上で自前のCPUを走らせることです。道は遠く長いです。そこで、先人の知見を辿って見ることにしましょう。RISCの教科書とも言える設計例として、(Z80を上回る量の)世界中に様々なソースがあります。またこの他に、CPUシミュレータ、教育用OSも沢山あります。ちょっと集めただけで以下のように沢山のCPUソースがあります。しかし、見ただけでは、機械語からCソースを想像するようなものでなにも分からないでしょう。( そこに本稿の存在意義があります。)
ニックネーム | 言語 | 特徴 |
YACC | Verilog | 拙作。5段パイプライン。 |
MIPSマイコンを作ろう | Verilog | リソース使用量はFZ80の1/2になったとのこと。 |
Plasma | VHDL | 3-5段パイプライン。自前RTOSでWEBサーバのデモあり |
Yellow star | 回路図 | 5段パイプライン。TLBを含む設計。 |
Ucore | Verilog | 5段パイプライン。 |
電通大 | Verilog | |
九工大 | VHDL | ソースを見るには登録が必要(登録しました) |
Minimips | VHDL |
まずは、要件を書き出してみます。
教科書は、
で、これは必須です。細かい命令セットの中身は東芝のTX-39ファミリーを見ればよいでしょう。
パイプライン割付検討
本物のLINUXを走らせるためにはMMUなしでは走りません。(uCLimux/uCOSU/ToppersはMMUなしでもOK)
とりあえずは、MMUは実装しないのですが将来のReserveが必要かどうか、MMUの有無がパイプライン段数に影響するかどうかを検討しました。
問題は、TLBとキャッシュにあります。
TLB:仮想インデックス物理アドレスに変換 −1CLK
CACHE:物理インデックスー物理タグ −1CLK
というシリアルな実装だと、2CLKかかってしまいます。オリジナル(R2000)では、これを半クロックづつ行いTLBとキャッシュがヒットしたときは、パイプラインは乱れません。今回のCPUは、POS EDGEのみを使うのでこのままではインストラクションとデータアクセスにそれぞれ2CLKとなり5段パイプが7段になってしまいます。ジャンプ時のDelayed SLOTも1CLKでは済まなくなるのでできれば避けたい実装です。(もしこれでやるとすれば予測分岐をしないとパフォーマンスが落ちます。)
そこで、
TLB:仮想インデックスから物理アドレスに変換 : CACHE 仮想インデックスから物理タグ比較 -1CLK(教科書109P)
と並列に行えば、1CLKで済みます。OS上の考慮も必要なようですが、OSでなんとかすることにしましょう。
パイプラインは、オリジナル通りの5段パイプラインとします。
YACCでは、Delayed SLOTが3SLOTもあり大変でしたが、今回はオリジナル通りの1SLOTのみとします。つまりジャンプ・分岐命令の直後の命令を分岐の有無にかかわらず1命令分実行します。全速力で走っている車が急には向きを変えられないように分岐後でも実行してしまいます。コンパイラは、ここに意味のある命令を置きますが、置けない場合は、NOP命令を置きます。
今回のクリティカルパスは、恐らくRFステージになると思います。レジスタファイルアクセス+分岐命令での次PCアドレス決定が複雑なロジックになる為です。
CPUからの外部アクセスは、インストラクション・データキャッシュにHitしない場合に起こります。この場合、外部RAMまたは、外部バスへのアクセスになるために、当然間に合わないので少なくとも1CLKはストールします。正確にはACKが返ってくるまでパイプラインをストールします。(YACCはこの機構がなかったので今回再設計しています。最初から考慮しておけばよかったです。ハイ)
IF | RF | ALU | MEM | WB | |
1CLK | インストラクションキャッシュフェッチアクセス | レジスタファイルリードアクセス | ALU演算 | データキャッシュアクセス | レジスタファイルライト |
1CLK | インストラクションキャッシュフェッチアクセス | レジスタファイルリードアクセス | ALU演算 | データキャッシュアクセス | レジスタファイルライト |
1CLK | インストラクションキャッシュフェッチアクセス | レジスタファイルリードアクセス | ALU演算 | データキャッシュアクセス | レジスタファイルライト |
... |
ところで、なぜ全命令で5段なのでしょうか? 命令によっては、ALU演算のみでMEMアクセスがない命令もあります。そのような場合には4段でもよいのではないかと思われるかもしれません。実際やってみるとわかるのですが、全ての命令で同じ段数というのがRISCのミソです。命令毎に段数が異なっているとリソースの競合(例えばALUが2個要る)が生じます。全ての命令でステージが揃っていれば、なにも考えることはなくて、リソースの競合がありません。
(下図参照)
t1 | t2 | t3 | t4 | t5 | t6 | t7 | t8 | ||||
1 | IF | RF | ALU | MEM | WB | ||||||
2 | IF | RF | ALU | MEM | WB | ||||||
3 | IF | RF | ALU | MEM | WB | ||||||
4 | IF | RF | ALU | MEM | WB | ||||||
5 | IF | RF | ALU | MEM | WB | ||||||
6 | IF | RF | ALU | MEM | WB | ||||||
7 | IF | RF | ALU | MEM | WB |
上図で、時刻t5では、5命令が並列に処理されています。一つの命令シーケンスとしては、5クロックを要するのですが、命令の処理速度(スループット)で見ると1CLK/1命令でプロセシングされることが分かります。マイクロプログラムCPUを上で書いてみましたが、シンクロナスRAMのREADは2CLKを要していました。ここでは、メモリをアクセスする命令もしない命令も全部順序よく5ステージとすることで、見かけ上1CLKのスループットを達成していることが分かると思います。
パイプラインステージとは、フリップフロップです。1ステージに(1個の)フリップフロップを置き状態を記憶します。つまりパイプラインを決めるとフリップフロップを置く位置が決まり、後は段間の組み合わせ回路を設計していけばよい訳です。それがRTL(Register Transfer Level)設計ということです。
それでは、具体的に5ステージ分のフリップフロップはどうなるでしょうか?
次のように書きました。
//stage0 reg [31:0] PC;//プログラムカウンタ //stage1 reg [31:0] IR;//インストラクションレジスタ //stage2 reg [31:0] IR_LR;//インストラクションパイプラインレジスタ reg [31:0] Sreg,Treg;//ALU前段 reg rf_write_lr; //stage3 reg [31:0] AReg;//ALU後段 reg [31:0] IR_A;//インストラクションパイプラインレジスタ //stage4 //レジスタファイル入力
後は、これらのレジスタの入力を書いていけばよいです。以降のソースは、これらのレジスタ(一時記憶)を中心に見ると分かりやすいと思います。
ここで、大事なことは、ブロック図を書くことです。(個人的には手書きが一番です。) いきなりHDLというのは、この位の規模ではありえません。ブロック図にすると、HDLよりは遅延パス等の見通しがよくなります。 (筆者は、今回ブロック図を2回修正しました。)
簡単なブロックの説明です。
[31:0] PC:プログラムカウンタ
[31:0] IR:インストラクションレジスタ
[31:0] IR_LR:IRを1CLK遅延させたパイプラインレジスタ
[31:0] IR_A: IR_LRを1CLK遅延させたパイプラインレジスタ
[31:0] S:ALU前段SourceRegister
[31:0] T:ALU前段TargetRegister
[31:0] A :ALU出力レジスタ
[31:0] RF [0:31] レジスタファイル
PCのアドレスを受けて、Icahceは、データ(インストラクション)をCPUに出力、IRで受ける。IR_LR,IR_Aは、単純にそれを1CLKづつ遅延させたパイプラインレジスタ。各パイプラインステージで必要となる制御信号は、これらのレジスタをデコードして作る。(設計の中身が理解し易いように敢えて単純な構成にしています。)
次に、パイプライン化で問題になる事項について見ていきましょう。
アーキテクチャの概説特に”歴史”の項をご参照ください。
ところで、U.S.では、多くの大学でMIPSアーキテクチャの授業があるようです。大変分かりやすい解説が多くありますので
それらのリンクを挙げておきます。
入門的な解説です。
http://www.eng.tau.ac.il/~arcorg/LECTURES/Lecture1.pdf
http://cs.nyu.edu/courses/spring07/G22.2233-001/lectures/lecture4-6.pdf
特に、パイプラインの解説が素晴らしいです。
http://www-inst.eecs.berkeley.edu/~cs152/fa04/lecnotes/lec6-2.ppt
Load遅延-メモリRead直後演算は、NOPになる
レジスタ参照は、フォワーディングのテクニックにより、パイプラインによる遅延を隠蔽することができますが、物理キャッシュやメモリの参照は、隠蔽することができません。具体的には、メモリRead直後の演算は間に合いません。コンパイラは、別な命令を挿入しようと頑張りますが、できない場合はNOPを挿入します。ハードウェアはこれについてなにも意識しません。次のディレイドスロットもハードウェアは特に意識する必要はありません。コンパイラ頑張れという感じです。このようにメモリ使用効率を犠牲にしつつ、コンパイラ技術で徹底的にシンプル(=高速)なハードウェアにするというのが初期MIPSの設計思想ではないでしょうか。
ディレイドスロットー車は急には、向きを変えられない
ジャンプ命令をいつ判断するのかというのが、重要なパイプライン設計の分かれ目です。一般に、パイプラインを深くすれば、ジャンプ命令の判断はそれだけ遅れ、パイプラインは進んでしまい(つまりPCがインクリメントされてしまい)ディレイドスロットが大きくなってしまいます。ある統計では、ジャンプ命令は、8命令に一回の頻度だそうです。旧YACCでは、動的ジャンプはDelayedSlot3でしたが、ここではオリジナルに忠実に実装しています。キャッシュ命令のReadに1CLK、ディレイドスロット1とすれば、自動的にジャンプ判断ステージは決まります。
t1 | t2 | t3 | t4 | t5 | t6 | t7 | t8 | ||||
1 | IF | RFここでジャンプアドレスを決定しても | ALU | MEM | WB | ||||||
2 | IFこの命令はすでにFetch。DelayedSlotになる(とにかく実行) | RF | ALU | MEM | WB | ||||||
3 | IFここでジャンプ先アドレス先命令 | RF | ALU | MEM | WB | ||||||
4 | IF | RF | ALU | MEM | WB | ||||||
5 | IF | RF | ALU | MEM | WB | ||||||
6 | IF | RF | ALU | MEM | WB | ||||||
7 | IF | RF | ALU | MEM | WB |
つまり DelayedSLOT1の場合、RF READステージで次のPCを決定しておく必要があります。ここのロジックは、実は最も複雑な部分なので、本当は、DelayedSLOTを3位にすれば、HW的にはもっともよいような気がしますが、そうするとコンパイラが全く有効な命令を挿入できない場合、JMP/NOP/NOP/NOPになってしまいます。まともにNOP命令を配置するとしたら、命令メモリ使用効率(キャッシュ効率)が悲惨になってしまいます。
フォワーディング
パイプラインCPUを設計すると必ず出てきます。
具体的なコードは、次です。教科書(30P参照)
qa/qb が本当のレジスタファイルの出力ですが、書き込まれるまでには、パイプラインを通過する分時間がかかります。したがって、レジスタファイルを読み出すのに生のqa/qbは直には使わず細工をしてやります。つまり、レジスタファイルから見て未来の書き込み結果を先取りして仮想のレジスタファイルRead出力(rf_target_out/rf_source_out)を作っています。
//フォワーディング回路 assign rf_target_out= (rf_dest_addr_lr==IR[20:16] && rf_write_lr && A_Right_SELD1==`A_RIGHT_ERT) ? AReg_input : (rf_dest_addr_a==IR[20:16] && rf_write_a && A_Right_SELD1==`A_RIGHT_ERT) ? rf_input : qa; assign rf_source_out=(rf_dest_addr_lr==IR[25:21] && rf_write_lr ) ? AReg_input : (rf_dest_addr_a==IR[25:21] && rf_write_a) ? rf_input : qb;
実は、この部分が遅延のネックの一部になっています。改善方法については、別途考えることにしましょう。
ストールとは?
今回は、外部とのインターフェースが必要ですので、ストール機構が必須です。ストール機構とは、パイプラインの一時停止機構です。 FPGA内RAMだけを使う限りにおいては、いつアクセスしてもR/W可能ですのでこの機構は必要ありません。(旧YACC) ところが、外部バスにつなげるとなるとこの機構は必須です。外部バスは、バス使用者(複数)がシェアをして使用しますので、バスアービタにお伺いをたてて使用権を獲得した後に初めて使用することができます。また、使用権を獲得しても、Req-Ackハンドシェークプロトコルによる場合には、いつAckが返ってくるかは、分かりません。この辺は、キャッシュコントローラがやってくれますが、CPUはその間待たされます。ということで、ストールは任意期間その状態(一時停止状態)を維持することが必要になります。このとき一時停止させるのは、一部のパイプラインレジスタではなくパイプライン全体であることに注意します。(走っている5両編成の電車の一車両だけが停止することはできないのと同じことです。)”停止”というのは、状態が変わらないということですから、状態遷移がない、つまり現在の状態を維持することを選択すればよいことになります。基本的なストール機構は、いわゆるストップモーションの一形態でつぎのような感じです。レジスタ+マルチプレクサで実現できます。
マルチプレクサで、ストールのときは、現状態を選択、ストールでないときは、次の状態をスルーにする回路を構成します。
HDLで書いた例としては、次のようになります。ソースを見ていただくと、パイプラインを構成する全レジスタについて同様の記述になっていることが分かると思います。(dstallがストール指示信号です。)
//ステージ1------------------------------------------------------------------------------ //stage 1 //インストラクションレジスタ always @(posedge clock) begin if (sync_reset) IR<=NOP_INSTRUCTION; else if (dstall); else if (int_recog) IR<=NOP_INSTRUCTION; else IR<=ir_out; end //ステージ2------------------------------------------------------------------------------- //IR_LR always @(posedge clock) begin if (sync_reset) IR_LR<=NOP_INSTRUCTION; else if (!dstall) IR_LR<=IR; end //RegWrite; always @ (posedge clock) begin if (sync_reset) rf_write_lr<=0; else if (dstall); else case (opcode_ir) ... //Sreg always @(posedge clock) begin if (sync_reset) Sreg<=0; else if (!(dstall)) Sreg<=rf_source_out; end //Treg always @(posedge clock) begin if (sync_reset) Treg<=0; else if (!dstall) begin case (A_Right_SELD1) Imm_signed : Treg<={ {16{IR[15]}},IR[15:0]}; Imm_unsigned : Treg<={ 16'h000,IR[15:0]}; Target_Reg_Output : Treg<=rf_target_out; IMM_26_SEL: Treg<={6'b00_0000,IR[25:0]}; default : Treg<={ {16{IR[15]}},IR[15:0]}; endcase end end ...
つまり、1段分のマルチプレスサが遅延パスに加算されます。実際初期のMIPSの設計思想は、パイプラインストールがないということが設計思想にあります(MIPSは、Microprocessor without Interlocked Pipeline
Stages(パイプラインステージがインターロックされないマイクロプロセッサ)の略)が、この一段分の遅延も惜しかったということなのでしょうか?
乗除算処理
ところで、命令に依存せず、パイプラインが常に動くかといういうとそうでもありません。具体的には、乗除算命令は、1CLKで実行できません。オリジナルでは、乗除算を実行させ、パイプラインは並列に次の命令を実行します。結果読み出し命令が来たときに、未だ実行中ならそのとき初めてストールします。これは、乗除算を計算中、他の処理をやらせて終わった頃に結果を拾おうという意図があると思いますが、そんなに巧くコンパイラが命令を配置できる訳ではなく大半は、待ちになっているように思います。ここでは、例外処理を考えると、難しくなるために、乗除算命令自体でストールさせる処理とします。また、オリジナルでは、乗除算中も割り込みを受付ますが、割り込みはストールが解除されるまで待ちとする仕様にしました。
t | IF | RF | ALU | MEM | WF | IFで乗除算命令発見 |
t+1 | IF | RF | ALU | MEM | WF | 次の命令はFetch、乗除命令ストールEnable |
t+2 | IF | RF | ALU | MEM | WF | 全ステージストール、直前命令がALU演算のところ。乗除算が直前の演算結果を参照するため。 |
t+.. | IF | RF | ALU | MEM | WF | 乗除命令実行 |
t+ | IF | RF | ALU | MEM | WF | 乗除命令終了、ストール解除 |
例外処理と割り込み処理は同じ
T.B.D.
外部バスでの性能低下を抑えるにはキャッシュが必要
YACCは、ハーバードアーキテクチャであり、プログラムアクセスとデータアクセスが分離、独立していました。
このためにプログラムとデータアクセスが同時に起きても問題ありませんでした。
汎用バス(たとえば、OpencoresのWishbone)を使おうとするとプログラムとデータは、同時にはアクセスできず何らかの調停機構を持って時分割で、バスをフェッチする必要があります。これによりCPUは、Wait動作をすることが必須となり、性能低下が顕著になります。またバスのフェッチはReq−Ackによるハンドシェークになるため単純なメモリのReadCycleは、常に3CLKを要してしまいます。(バーストアクセス機構については、今回は設計しません。) ということで、1/2〜1/6の性能低下が見込まれます。汎用バス形式をとることにより、IPコア(SDRAMコントローラやEthernetネットコントローラ)を接続することが容易な反面、このままでは性能低下は避けることができません。なんとか性能低下を最小に食い止める方法はないでしょうか?
そのためには、キャッシュを導入します。CPUの側にインストラクションとデータの専用キャッシュを設ければ、少なくともReadアクセスについてはキャッシュがヒットする限り、今までどおり独立にアクセスが可能です。キャッシュで交通の大半を食い止めることができれば、外部バスでの交通渋滞を避けることが出来ます。キャッシュ方式は、最も簡単な方法で設計しようと思いますが、それでも、"あり"と"なし"では上での考察の通り、かなりの性能差が予想されます。
キャッシュ設計仕様
命令キャッシュ・データキャッシュ仕様
上の仕様で設計してみましょう。
<キャッシュ無効化領域指定>
まず、キャッシュ無効化領域については、HDLコンパイル時に指定するものとします。(オリジナルでは固定化されていますが、現状MMUを備えていないのと、32ビットアドレス空間で設定するのも冗長なので。)IO領域では、同じアドレスをR/Wしても同じ値が返ってくるとは限りません。つまりキャッシュは使えません。そこでそのような領域は、キャッシュが介在せず、CPU−バス間でアクセスを行うようにします。
<リフィルアルゴリズム>
ライトバックポリシーOnlyとしますが、ライトバック・リフィルは簡単のため、単にSingleR/Wルーチンを呼び出す仕様とします。また、簡単のため、ライト用FIFOも備えません。
キャッシュの状態から考えましょう。リセット時は、キャッシュの状態は不定でInvalidになります。Invalid状態からのReadは、常にキャッシュミスとなり、まず、StallをCPUに指示します。そのあと、外部バスの方を向いてリフィルします。リフィル後はCleanな状態になります。
Invalid状態でのWriteは、常にWriteミスとなり、リフィル後にキャッシュライトしDirty状態になります。このとき外部メモリの書き戻し(ライトバック)は、行いません。ライトバックは、Dirty状態でのキャッシュミス時のみ行いキャッシュミスしたアドレスでのLRU操作、ライトバック、リフィル動作とするシーケンスを作ります。その間、CPUに対しては、ストールを維持し、リフィル後にStallを解除し、最後に要求R/W動作を完了します。(CPUはその間、待たされるので、本来はバースト的に外部アクセスしたいところです。)
なお、インストラクションキャッシュについてはReadOnlyですから、(自己書き換えは不可とする仕様)Dirty状態は存在しません。Dataキャッシュのみがライトバックできる仕様とします。
キャッシュヒット時は、StallをアサートしないでそのままR/Wします。
以上から、キャッシュの状態として
が存在します。状態を遷移させる条件としては、
があります。これらで、メインルーチン(メインシーケンサ)を構成します。
またtaskとして
があり、これらは、メインルーチンからの指示を受けて、stb_o や、cycle、we_oを生成し、ack_iを受け取るサブシーケンサになります。
この辺はソフト的なサブルーチンをHWで実装するような感じですね。 結構複雑なステートマシンになるかもしれません。
想定する外部バス仕様
とりあえずOpencoresのWishbone のmin.仕様を想定します。Section3.1.3に基本的なハンドシェークのプロトコルが載っています。単純なREQ-ACKです。
YACCは、二つのMasterポートを持ち外部メモリ(SDRAMコントローラ)は、SLAVEとします。調停は、Wishboneバスコントローラにお願いすることにして、二つのMasterは、独立に動作させます。(お互いのことは関知しません。Wishboneバスコントローラにお任せです。)
これですと、最も速くて2CLK/1バスサイクルになりますね。ちなみに次は8ビットGPIOのMinimum Slaveの例ですが、次のように簡単です。
しかし、FPGAの同期RAMのReadは、アドレスを渡した次のクロックでしかACKは返せないために、さらにもう1バスサイクルが必要になります。SDRAMコントローラのSLAVEでは、当然バーストオペレーションプロトコルを使いたいところですが、今回は設計しません。(サブシーケンサを後で置き換えればよいでしょう。他のバスにつなげるときもサブシーケンサ部分の書き換えで対応することにしましょう。)
<タグRAM仕様>
データタグRAM
Cache Status | Way 0 | Way 1 | ||||||
Address | LRU | Lock | Valid | Dirty | Phy [31:12] | Valid | Dirty | Phy [31:12] |
0 | ||||||||
... | ||||||||
... | ||||||||
... | ||||||||
... | ||||||||
255 |
Lockは、Reserveのみで機能は実装していません。LRUは、cacheにヒットしなかったとき、ReplaceされるWayを示します。
Validは、1でVailid、0でInvalidを示しPowerOn 時0で、cacheがリフィルされたときに1になります。Dirtyは、一度でもライトされると1になります。リフィルされると0(clean)になります。初期値は、FPGAのDefaultの初期化0を前提にしています。
インストラクションタグRAM
Cache Status | Way 0 | Way 1 | ||||||
Address | LRU | Lock | Valid | Reserve | Phy [31:13] | Valid | Reserve | Phy [31:13] |
0 | ||||||||
... | ||||||||
... | ||||||||
... | ||||||||
... | ||||||||
511 |
インストラクションキャッシュ (big endian only)
8bit x2048 words x4(bytes/word) x2(way)
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | |
0 | [31:24] | [23:16] | [15:8] | [7:0] | [31:24] | [23:16] | [15:8] | [7:0] | [31:24] | [23:16] | [15:8] | [7:0] | [31:24] | [23:16] | [15:8] | [7:0] |
0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 2 | 2 | 2 | 2 | 3 | 3 | 3 | 3 | |
4 | 4 | 4 | 4 | 5 | 5 | 5 | 5 | |||||||||
511 | 127 | 2044 | 2044 | 2044 | 2047 | 2047 | 2047 | 2047 |
データキャッシュ (big endian only)
8bit x1024 words x4(bytes/word)x2(way)
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | |
0 | [31:24] | [23:16] | [15:8] | [7:0] | [31:24] | [23:16] | [15:8] | [7:0] | [31:24] | [23:16] | [15:8] | [7:0] | [31:24] | [23:16] | [15:8] | [7:0] |
0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 2 | 2 | 2 | 2 | 3 | 3 | 3 | 3 | |
4 | 4 | 4 | 4 | 5 | 5 | 5 | 5 | |||||||||
255 | 127 | 1020 | 1020 | 1020 | 1023 | 1023 | 1023 | 1023 |
<キャッシュアルゴリズム>
具体的なキャッシュアルゴリズムは以下です。
if( VIRTUAL ADDRESS(TLBサポート時、現在はPhy)が非キャッシュ領域なら){
非キャッシュシーケンス
}else if (キャッシュヒットなら){
以下のキャッシュヒットシーケンスを駆動 バスサイクルなし
キャッシュにたいしてR/W,外部バスにはアクセスしない
write => dirty bit をONにする
read => dirty bit はそのまま
LRU マークをstatus に書き込み
}else {
assert (キャッシュミス);
LRU selection
if (invalid || clean) リフィル->キャッシュヒットシーケンス
else {
assert (dirty);
ライトバック->リフィル ->キャッシュヒットシーケンス
} }
他のバスに移植する場合は、非キャッシュシーケンス、ライトバックシーケンス、リフィルシーケンスを置き換えればよいです。
具体的なHDLコードは次です。
//ステート定義 localparam [1:0] HitState=2'b00 , //メインシーケンサステート定義 NonCacheDomain=2'b01, CacheRefil=2'b10, CacheWriteBack=2'b11; localparam[1:0] Idles=2'b00, //非キャッシュ領域ステート定義 StallState=2'b01, StallMaskState=2'b11; localparam [1:0] Idlew=2'b00, //ライトバックステート定義 WriteStrobe=2'b01, WriteAckReceived=2'b11, WriteBackFinished=2'b10; localparam [1:0] Idler=2'b00, //リフィルステート定義 ReadStrobe=2'b01, ReadAckReceived=2'b11, ReadFillFinished=2'b10; //ステートマシンフリップフロップs reg [1:0] cache_seq; reg [1:0] non_cache_domain_seq; reg [1:0] refill_seq; reg [1:0] writeback_seq;
メインシーケンサのコードです。
//メインシーケンサステートマシン always @(posedge clock) begin if (sync_reset) cache_seq<=0; else if ( !mul_div_stall ) case (cache_seq) HitState: if (non_cache_domain ) cache_seq<=NonCacheDomain; else if (!cache_hit && (cache_write | cache_read) ) begin if (!lru_valid || !lru_dirty) cache_seq<=CacheRefil; else cache_seq<=CacheWriteBack; end NonCacheDomain: if (non_cache_domain_seq==StallMaskState && !non_cache_domain ) cache_seq<=HitState; CacheRefil: if (refill_seq==ReadFillFinished) cache_seq<=HitState; CacheWriteBack: if (writeback_seq==WriteBackFinished) cache_seq<=CacheRefil; endcase end
ライトバックシーケンスです。
//ライトバックシーケンサステートマシーン reg [2:0] writeback_data_counter; localparam integer Limit_Count=4;//ライトバック単位は4ワード(ワード単位は4Byte) always @(posedge clock) begin if (sync_reset) writeback_seq <=Idlew; else case (writeback_seq) Idlew:if (cache_seq==CacheWriteBack) writeback_seq<=WriteStrobe; WriteStrobe: if (ack_fm_ext ) writeback_seq<= WriteAckReceived; WriteAckReceived: if (!ack_fm_ext ) begin if (writeback_data_counter <Limit_Count) writeback_seq<=WriteStrobe; else writeback_seq<= WriteBackFinished; end WriteBackFinished: writeback_seq <=Idlew; endcase end
リフィルシーケンスです。
//リードフィルシーケンサステートマシーン reg [2:0] read_data_counter; always @(posedge clock) begin if (sync_reset) refill_seq <=Idlew; else case (refill_seq) Idler:if (cache_seq==CacheRefil) refill_seq<=ReadStrobe; ReadStrobe: if (ack_fm_ext ) refill_seq<= ReadAckReceived; ReadAckReceived: if (!ack_fm_ext ) begin if (read_data_counter <Limit_Count) refill_seq<=ReadStrobe; else refill_seq<= ReadFillFinished; end ReadFillFinished: refill_seq <=Idler; endcase end
非キャッシュシーケンスのコードです
//非キャッシュ領域シーケンサ always @(posedge clock) begin if (sync_reset) non_cache_domain_seq<=Idles; else if( !mul_div_stall ) case (non_cache_domain_seq) Idles: if (non_cache_domain) non_cache_domain_seq<=StallState; StallState: if (ack_fm_ext) non_cache_domain_seq<=StallMaskState; StallMaskState: if (non_cache_domain) non_cache_domain_seq<=StallState; else non_cache_domain_seq<=Idles; default: non_cache_domain_seq<=Idles; endcase end
Wishbone バスコントローラ バスアービタは、Opencores Conbus IPを使用します。Priorityは設定できません。単純なラウンドロビンでつぎのソースがそのエッセンスです。
デバッグ中の様子です。ScopeViewでconbus に、Wishboneのアービタ、Slaveとしての外部RAM(仮想モデル)、 YACCの二つのキャッシュポート(Master)が接続していることが分かります。Wishboneコントローラが、Masterのバス獲得要求を受けてRoundRobinでマスターを切り替えてくれています。
RAMのWishbone接続モデルは、Openriscのそれを使っています。バス幅は同じ32ビットなのですが、バイトアクセスモデルが違うので少し改造が必要です。 ちなみにOpenriscのキャッシュはダイレクトマップです。(どうしてLRUにしなかったのかは分かりません。 Microblazeもダイレクトマップ、ライトスルーですし、特許の関係があるのかもしれません。この辺は未だ枯れていない領域だと思います。)
ところで、Microblazeの話が出たので、インストラクションフォーマットについて一言。オリジナルのフォーマットは3タイプに分かれます。最初のR-Typeは、演算系で、例えば、
R1<=R2+R3;
の3項演算です。次のItypeは、即値Typeです。32ビットの即値は、2命令のマクロ命令になります。最後は、ジャンプTypeです。命令はこの3つのどれかに入りますので、デコードが非常に簡単です。上のマイクロプログラムCPUとも似たフォーマットです。
R-Type:
31-26 | 25-21 | 20-16 | 15-11 | 10-6 | 5-0 |
opcode (000000) | rs | rt | rd | shamt | funct |
I-Type
31-26 | 25-21 | 20-16 | 15-0 |
opcode | rs | rt | immed. |
J-Type
31-26 | 25-0 |
opcode | target |
一方、Microblazeは、この資料によると次のようになっています。なんか似ていますね。最初は、パイプラインも3段だったのですが、最近のは5段になっています。
0-5 | 6-10 | 11-15 | 16-20 | 21-31 | |
opcode | rd | rsA | rsB |
0-5 | 6-10 | 11-15 | 16-31 |
opcode | rd | rs | immed. |
YACCから、変更するのもそんなに難しいことではないでしょう?(筆者にそのような意図や計画はありません。)
下は、プログラムループで同じところをアクセスしているのですが、キャッシュがヒットすると、外部RAMにはアクセスしに行かなくなることが分かります。それにしても、ヒットしないとハンドシェークで、やりとりするのでペナルティは大きいです。
キャッシュの性能測定
ドライストーンベンチでのキャッシュ容量と速度比の測定結果です。ドライストーンは、16KBもあれば、プログラムデータ共収まってしまう位に小さいベンチです。(ドライストーンベンチは、パッケージにある、プログラム中の試行回数を1000回実行した結果です。ジャンプの無駄SLOTがない分、旧YACCより、10-20%MIPSは高くなっています。)しかし、2KB以降、速度向上は見られません。(紫) 一方浮動小数点演算MAC(エンジ)の方は、64KB以上あるプログラムですが、16KB以上でも速度向上しそうな気配です。なお、ここでいうキャッシュ容量はデータキャッシュ、インストラクションキャッシュ各々の値です。
CPUシミュレータを作る
今まで、CPUシミュレータは使用しておりませんでした。しかし、色々な意味でCPUシミュレータはあった方がよいです。第一に、波形付RTLシミュレータの30倍は速いので、RTLデバッグのリファレンスとして使えます。問題があったら、結果を比較して問題時刻箇所を絞り込みます。ここでいう結果比較とは、PCとメモリに対する読み書きデータです。(レジスタについては、パイプラインのDelayがありますので、必ずしも一致した結果にはなりません。(NOPが数クロック続いた後では一致します。))後は、WaveformViewer上で、問題時刻周辺のRTL動作をチェックしてデバッグします。
作ると言っても、先人が作ったものがありますので、それを利用します。ただし、YACCは、まだTLB変換をサポートしていないのと、IO空間、例外割り込み関係は、YACC独自の空間定義にしています。その辺は、改造しなければいけません。CPUの動きをシミュレートしてくれるものであれば、なんでもよいのですが数あるシミュレータ (ここにもあります) から、改造しやすいものを選びます。選定の要点は、
以上を考慮して、YAMSというシミュレータのソースを利用させていただくことにしました。
以下は、簡単なCプログラムでの実行例です。
改造後のYAMSによるシミュレーションです。DOS窓に出力しています。
同じCプログラムをRTLシミュレーションした様子です。
YAMSのファイル出力です。
0 PC= 0 IR=3c1c0000 : LUI $gp, 0 1 PC= 4 IR=379cef30 : ORI $gp, $gp, ef30 2 PC= 8 IR=3c040000 : LUI $a0, 0 R[28]=ef30 3 PC= c IR=348470e4 : ORI $a0, $a0, 70e4 4 PC= 10 IR=3c050000 : LUI $a1, 0 R[ 4]=70e4 5 PC= 14 IR=34a570f8 : ORI $a1, $a1, 70f8 6 PC= 18 IR=3c1d001f : LUI $sp, 1f R[ 5]=70f8 7 PC= 1c IR=37bdff00 : ORI $sp, $sp, ff00 R[29]=1f0000 8 PC= 20 IR=ac800000 : SW $zero, 0 ($a0) R[29]=1fff00 Store_Long adr= 70e4 data= 0 9 PC= 24 IR= 85182a : SLT $v1, $a0, $a1 10 PC= 28 IR=1460fffd : BNE $v1, $zero, 18 R[ 3]=1 11 PC= 2c IR=24840004 : ADDIU $a0, $a0, 4 12 PC= 20 IR=ac800000 : SW $zero, 0 ($a0) R[ 4]=70e8 Store_Long adr= 70e8 data= 0 13 PC= 24 IR= 85182a : SLT $v1, $a0, $a1 14 PC= 28 IR=1460fffd : BNE $v1, $zero, 18 15 PC= 2c IR=24840004 : ADDIU $a0, $a0, 4 16 PC= 20 IR=ac800000 : SW $zero, 0 ($a0) R[ 4]=70ec
Veritakのファイル出力です。
0 PC=00000000 IR=00000000 1 PC=00000004 IR=3c1c0000 2 PC=00000008 IR=379cef30 3 PC=0000000c IR=3c040000 4 PC=00000010 IR=348470e4 5 PC=00000014 IR=3c050000 6 PC=00000018 IR=34a570f8 7 PC=0000001c IR=3c1d001f 8 PC=00000020 IR=37bdff00 9 PC=00000024 IR=ac800000 10 PC=00000028 IR=0085182a Store_Long addr=000070e4 data=00000000 11 PC=0000002c IR=1460fffd 12 PC=00000020 IR=24840004 13 PC=00000024 IR=ac800000 14 PC=00000028 IR=0085182a Store_Long addr=000070e8 data=00000000
これは、CPUソース中、次のソースによる出力です。
`define DEBUG `ifdef DEBUG integer fi; `ifdef REFERENCE_SIM initial fi=$fopen("pc_file_ref.txt","w"); `else initial fi=$fopen("pc_file.txt","w"); `endif initial debug1.counter=0; reg [31:0] load_address_ir,load_address_ir_lr,load_address_ir_a; always @(posedge clock) begin:debug1 integer counter; integer i; if (~dstall & pre_rw_access) load_address_ir_lr<=pre_mem_addr; if(!dstall) load_address_ir_a<=load_address_ir_lr; if (!dstall ) begin $fdisplay(fi,"%6d PC=%8h IR=%8h ",counter,PC,IR); counter<=counter+1; `ifdef REG_DEBUG_OUT for (i=0;i<=31;i=i+1) begin $fwrite(fi,"R[%02h]=%h ",i,ram_regfile32xx32.mem[i]); end $fdisplay(fi,""); `endif end if (mem_write_req && !dstall) begin if (opcode_ir_lr==StoreByte ) $fdisplay(fi,"Store_Byte addr=%8h data=%2h",load_address_ir_lr,data_to_mem[7:0]); else if (opcode_ir_lr==StoreWord ) $fdisplay(fi,"Store_Word addr=%8h data=%4h",load_address_ir_lr,data_to_mem[15:0]); else $fdisplay(fi,"Store_Long addr=%8h data=%8h",load_address_ir_lr,data_to_mem[31:0]); end if (mem_read_req && !dstall) begin if (opcode_ir_a==LoadByteSigned || opcode_ir_a==LoadByteUnsigned ) $fdisplay(fi,"Load_Byte addr=%8h data=%2h",load_address_ir_a,data_to_mem[7:0]); else if (opcode_ir_a==LoadWordSigned || opcode_ir_a==LoadWordUnsigned ) $fdisplay(fi,"Load_Word addr=%8h data=%4h",load_address_ir_a,data_to_mem[15:0]); else $fdisplay(fi,"Load_Long addr=%8h data=%8h",load_address_ir_a,data_to_mem[31:0]); end end `endif
ドライストーンベンチの最後まで行ったところです。左がVeritak出力、右がCPUシミュレータ出力です。
Veritakの方は、IRが1Clock遅れ、Storeが2Clock遅れ、Loadが3Clock遅れで一致しています。簡単には秀丸の”同時スクロール”で見比べてもよいですし、勿論、スクリプト等で、ファイルをコンペアしてもよいです。
特に、大きなCプログラムで「動かない」ときに威力を発揮する強力なツールになる筈です。(幸いまだお世話になっていません。)
printf を実装してみる
アドレス空間が大きいと(4GBありますが、とりあえずシミュレーション環境は2MBにしています。)メモリ節約感覚はなくなりつつあり、少し真面目なprintfが欲しくなります。組み込み用OSでは、大抵浮動小数表示は非サポートなので参考になるソースがあまりなかったのですが、ReactOS関係でありました。有難く移植させていただきました。
Cプログラムはこんな感じで、浮動小数も書式指定ができます。
void main() { unsigned long i; double d,d1; d=9.03033e2; for (i=0;i<10;i++){ d *=2.304371e-1; printfy("i=%3d Exp. Disp:d=%4.9e Floating Disp:d=%3.7f\n",i,d,d); } print("$finish"); }
CPUシミュレータは、こんな風に出力します。スタックポインタは、2MB-0x100番地に設定しています。CPUシミュレータ上では、キャッシュの存在は影響しません。
RTLシミュレーションの様子です。キャッシュを動かしています。
ファイルの入出力を考える
少し大きなプログラムを動かそうとするとファイルを読み書きしたくなります。たとえば、BMP画像を読み込んで、JPG画像を出力させたい、としたらどうすればよいでしょうか?なんとかOSなしにファイルを読み書きしたいです。
ここでは、仮想になりますが、やはりポートを叩いてファイルを入出力させることを考えます。
インターフェースは、CPUコアで行うことにしました。
malloc/free を実装する
やはり、少し大きなプログラムを動かそうとすると必要になります。これもなんとかしましょう。余談ですが、mallocは奥が深くVeritakでもRun-Timeエンジンは自作のmalloc版を使っています。(prexのカーネル部の実装も参考になります。prexは、いつかポートしたいですね。(リトルエンディアンに書き換えないと..))ここでは、古典的・汎用的なK&R版を使いました。
mallocとファイル入出力に関するサンプルです。
malloc/free/printf/fopen/fputc/fwrite/fgetc/fread/memcpy/memset.. 等は、YACCに専用のものをヘッダファイルで呼び出す必要があります。使い方は、標準Cと大体同じですが若干違います。(使い方はパッケージ中のソースを眺めてください。)特に注意する点は、mallocを使う際は、一度だけ初期化が必要な点です。
#include "yacc_port.h" #include "printy.h" #include "malloc.h" #include "fopen.h" char buf[400]; char src_buf[100]="This is a Pen.\n Thanks a lot!\n "; void main(void) { int fi,ch,i; unsigned char* read_ptr; unsigned bytes; void *heap_start; unsigned size; fi=fopen("first.txt","wb"); for (i=0;i<sizeof(src_buf);i++){ if (!src_buf[i]) break; fputc(fi,src_buf[i]); } fclose(fi); memset(buf,sizeof(buf),0); fi=fopen("first.txt","rb"); i=0; ch=fgetc(fi); while (ch!=EOF){ buf[i++]=ch; ch=fgetc(fi); } printf("Read text=%s",buf); fclose(fi); printf("Reading BMP file..\n"); heap_start=HEAP_START; size=(PHYSICAL_MAX-STACK_SIZE-HEAP_START); MallocInit(heap_start,size); read_ptr=malloc(400*1000); printf("read_ptr=%x",read_ptr); fi=fopen("cmain.bmp","rb"); bytes=fread(read_ptr,1,400*100,fi); fclose(fi); printf("Done bytes=%d\n",bytes); fi=fopen("cmain_copy.bmp","wb"); bytes=fwrite(read_ptr,1,bytes,fi); fclose(fi); printf("Done Copy\n"); free(read_ptr); print("$finish"); }
実装の中身は、いたって簡単です。(それにしてもCのヘッダファイルとRTLのポートの定義を共有(メンテ)したいと思うのは私だけでしょうか?)
int fopen(const char* file_name,const char* rw) { unsigned ch; unsigned *ptr; ASSERT(rw); ASSERT(file_name); if (!strcmp(rw,"w")|| !strcmp(rw,"wb")) ptr=FILE_NAME_WRITE_PORT_ADDRESS; else ptr=FILE_NAME_READ_PORT_ADDRESS; while (*file_name){ ch=*(file_name++); *(volatile unsigned *)ptr=ch ; } *(volatile unsigned *)ptr=0 ;//Write zero as string end mark. ch=*ptr; if (ch ==EOF) { printf("file open error\n"); print("$finish"); } ASSERT(ch >=MIN_FILE_DESCRIPTOR && ch <=MAX_FILE_DESCRIPTOR); return ch; } inline int fputc(int file_descriptor, unsigned char ch){ unsigned *ptr=FILE_IO_START+file_descriptor*4; *(volatile unsigned *)ptr=(unsigned)ch ; return 0;//Sorry,No check } inline int fgetc(int file_descriptor){ unsigned ch; // ASSERT(file_descriptor >=MIN_FILE_DESCRIPTOR && // file_descriptor <=MAX_FILE_DESCRIPTOR); unsigned *ptr=FILE_IO_START+file_descriptor*4; ch=*ptr; return ch;// } void fclose(int file_descriptor){ ASSERT(file_descriptor >=MIN_FILE_DESCRIPTOR && file_descriptor <=MAX_FILE_DESCRIPTOR); unsigned *ptr=FILE_IO_START+file_descriptor*4; *(volatile unsigned *)ptr=(unsigned)EOF ; } unsigned fread(unsigned char *buf, unsigned size_per_word, unsigned words, int fp) { unsigned bytes; unsigned i; unsigned c; bytes=words* size_per_word; for (i=0;i<bytes;i++){ c=fgetc(fp); if (c==EOF){ return i; } buf[i]=c; } return bytes; } unsigned fwrite(unsigned char *buf, unsigned size_per_word, unsigned words, int fp) { unsigned bytes=size_per_word* words; unsigned i; for (i=0;i<bytes;i++){ fputc(fp,buf[i]); } return bytes;//Sorry, no check! }
RTL側は、仮想ポートです。ポートのライト(printf等)は、書きっぱなしなので大したことはないのですが、ポートのリード(fgetc等)は、少し工夫が要ります。仮想のポートなので、仰々しくハードウェアを書きたくはありません。やりたいことは、キャッシュからCPUに返すバスを横取りして値を返してやればよいだけです。横取りをどう書けばよいかというと、force/releaseを使います。(force/release
は、シミュレータのリソースを食うし遅くなるので多用は避けたいのですが、ここはしょうがないです)
//仮想ファイルポート reg [0:8*100-1] file_name_buffer; integer file_name_char_counter=0; //fopen後のファイルディスクリプタを保持して次のReadで返す integer last_file_descriptor=`MIN_FILE_DESCRIPTOR-1; //実ファイルディスクリプタを保持 integer file_descriptors[`MIN_FILE_DESCRIPTOR:`MAX_FILE_DESCRIPTOR]; //ローカル変数 integer i; integer index_offset; integer fi; integer char; integer forced_flag=0; always @ (posedge clock ) begin : Virtual_FIle if ((mem_write_req === 1'b1) && ! (ir_stall || to_dcache_wait_req || dstall) ) begin if (mem_addr_d==`FILE_NAME_WRITE_PORT_ADDRESS || mem_addr_d==`FILE_NAME_READ_PORT_ADDRESS ) begin//ファイルネームポートライトアクセス if (data_fm_cpu[7:0]===8'h00) begin //LONG アクセス file_name_buffer[file_name_char_counter*8 +:8]=data_fm_cpu[7:0]; if (mem_addr_d==`FILE_NAME_READ_PORT_ADDRESS)begin fi=$fopen(file_name_buffer,"rb"); end else fi=$fopen(file_name_buffer,"wb"); if (fi) begin last_file_descriptor=last_file_descriptor+1; if (last_file_descriptor>`MAX_FILE_DESCRIPTOR) begin $display("Excessive max files."); $finish; end file_descriptors[last_file_descriptor]=fi; end else begin $display("can not open %s",file_name_buffer) ; $finish; end for (file_name_char_counter=0; file_name_char_counter< 100; file_name_char_counter=file_name_char_counter+1) begin file_name_buffer[file_name_char_counter*8 +:8]=8'h00; end file_name_char_counter=0; end else begin file_name_buffer[file_name_char_counter*8 +:8]=data_fm_cpu[7:0]; file_name_char_counter=file_name_char_counter+1; end end else if ( mem_addr_d >= `FILE_IO_PORTS && mem_addr_d < (`FILE_IO_PORTS +4*`NUM_OF_FILE_DESCRIPTORS)) begin//ファイルIOポートライトアクセス index_offset=$unsigned(mem_addr_d-`FILE_IO_PORTS+4*`MIN_FILE_DESCRIPTOR)>>2; if (!file_descriptors[index_offset]) begin $display("file_error"); $stop; $finish; end fi=file_descriptors[index_offset]; if (data_fm_cpu ==`EOF)begin $fclose(fi);//EOF を書かれたらfcloseする仕様 end else $fputc(fi,data_fm_cpu[7:0]);// end end //if write end //always always @ (negedge clock ) begin//force /release を確実にするために半CLOCKずらす if (forced_flag && ! (ir_stall || to_dcache_wait_req || dstall) ) begin release data_to_cpu; forced_flag=0; end if ((mem_read_req === 1'b1) && ! (ir_stall || to_dcache_wait_req || dstall) ) begin if (mem_addr_dd==`FILE_NAME_WRITE_PORT_ADDRESS || mem_addr_dd==`FILE_NAME_READ_PORT_ADDRESS ) begin//ファイルネームポートライトアクセス force data_to_cpu=last_file_descriptor; forced_flag=1; end else if ( mem_addr_dd >= `FILE_IO_PORTS && mem_addr_dd < (`FILE_IO_PORTS +4*`NUM_OF_FILE_DESCRIPTORS)) begin//ファイルIOポートライトアクセス index_offset=$unsigned(mem_addr_dd-`FILE_IO_PORTS+4*`MIN_FILE_DESCRIPTOR) >>2; if (!file_descriptors[index_offset]) begin $display("file_error"); $stop; $finish; end fi=file_descriptors[index_offset]; char=$fgetc(fi); force data_to_cpu=char; forced_flag=1; end end //if read end
RTLシミュレーション結果です。
CPUシミュレーション結果です。
Cygwin環境でのGNU Cコンパイラの構築
まずは、必要なソースコードをダウンロードします。今回は、
binutils が2.16、GCCが4.1.2,newlibが1.15.0の組み合わせでビルドしました。それ以外の組み合わせでは失敗するかもしれません。
手順としては、binutilsでアセンブラを構築、GCCのコンパイルを行い、出来上がったGCCでnewlibを構築する3ステップの手順になります。私の環境では、一日以上かかりました。(コンパイルエラーの出ない組あわせを探す作業も含んでいます。) 必要なEXEファイルはパッケージでアップしますので、敢えてビルドする必要はありません。
ポイントは、エンディアンの指定とsoft-floatの指定の二つです。エンディアンは、mips-elf
を選択するとビッグエンディアンになります。またconfigure で--without fp
を指定するとsoft-float を生成してくれます。(これがないとfpu(コプロセッサ1)の命令を生成してしまうので動きません。)
下では、メインのDirectory下に3つのDirectoryを作りその下でコンパイルを行っています。
Step1 Build binutils
% tar xjfv binutils-2.16.tar.bz2 % mkdir build-binutils % cd build-binutils % export TARGET=mips-elf % export PREFIX=/usr/local/$TARGET % export PATH=$PATH:$PREFIX/bin % ../binutils-2.16/configure --target=$TARGET --prefix=$PREFIX % make % make install
なぜか、スクリプトだとうまくいかなかったので、キーボードを叩いて入力しました。アセンブラはusr/local/mips-elf/bin
下に出来ています。
Step2. Build GCC
上のパス設定が生きた状態で作業します。configure を間違えたときは、build-gcc(作業領域)の内容を消去して再実行します
% tar xjfv gcc-4.1.2.tar.bz2 % mkdir build-gcc % cd build-gcc % ../gcc-4.1.2/configure --without-fp --with-newlib --with-headers=../newlib-1.15.0/newlib/libc/include --enable-languages="c++" --target=$TARGET --prefix=$PREFIX % make % make install
これでGCCが出来上がりました。(欲張ってC++まで入れてみました。)
Step3. Build newlib
newlib は組み込み用のCライブラリです。上のパス設定が生きた状態で作業します。
% tar xzfv newlib-1.15.0.tar.gz % mkdir build-newlib % cd build-newlib % ../newlib-1.15.0/configure --without-fp --target=$TARGET --prefix=$PREFIX % make % make install
以上でコンパイラの構築終了です。
一つの問題
上記コンパイラで、-msoft-float スイッチでコンパイルするとsoft float の命令を生成してくれます。
が、問題があります。
YACCは、先達に習い,パテントになっている命令(lwr,lwr,swr,swl)を実装していません。(plasma
や、yellow starも同様です。勿論、これを回避したからといって、10万件あると言われるCPUのパテントに対し、パテントフリーということではありません。自己責任にてお願いします。) ところが、上記コンパイラは生成してしまいます。オプションスイッチも見たのですが、残念ながらそのようなスイッチはありませんでした。一つの案としては、未定義命令で例外を発生させてハード的にソフト処理させる手がありますが難しそうなので、ブートストラップのファイルをいじってコンパイラが生成しないようにしました。
いじるファイルはGCCのソース中、
です。これらはパッケージに含まれます。
もう一つの問題
それでも、生成された逆アセンブルリストを見ると、未だ入っていました。どうやら、libc中のmemcpyで、使われています。memcpy.cは、一部アセンブラで書かれていました。(その他にはないようです。) これについては、自分で書いたmemcpyをリンクさせることにしました。
Cコンパイラを使ってみる-cpuとキャッシュの検証
ソースは、以下の数行です。c=56と2800の二つのVersionがあり出力する桁数が違います。c=2800の方は、800桁出力となりRTLも時間がかかります。
include "yacc_port.h"
#include "printy.h"
#include "malloc.h"
//long a=10000,b,c=56,d,e,f[57],g;
long a=10000,b,c=2800,d,e,f[2801],g;
void main()
{
for(;b-c;)f[b++]=a/5;for(;d=0,g=c*2;c-=14,printf("%d",e+d/a),e=d%a)for(b=c;d+=f[b]*a,f[b]=d%--g,d/=g--,--b;d*=b);
print("$finish");
}
Cソースです。
#include "yacc_port.h" #include "printy.h" #include "malloc.h" void main(void) { short i; double x, y1, y2, pi; double dx = 1.0; /* 浮動小数点数の初期化には小数点を付ける */ printf("pi calculation by integral method."); for (i = 0; i < 2; i++) { pi = 0.0; /* pi と y1 は、ここで初期化する */ y1 = 1.0; /* y1 = 0.0 となっていたのを、訂正 */ dx /= 10; /* ループを回る度に dx は 1/10 になる */ for (x = 0.0; x <= 1.0 - dx; x += dx) { /* x < 1.0 - dx; となっていたのを、訂正 */ y2 = sqrt(1 - (x + dx) * (x + dx)); pi += (y1 + y2) * dx / 2; /* 台形の面積を求める */ /* 計算で求めた y2 の値を y1 に代入し */ y1 = y2; /* 次のループでもう一度使う */ } /* dx と pi の値を表示 */ printf("dx = %7.5f\tPi = %10.8f\n", dx, pi * 4.0); } print("$finish"); }
CPUシミュレータ出力です。
RTLシミュレーションです。
(225,223)リードソロモンです。イレージャ10箇所、訂正11箇所のエラーをつくり、誤訂正がないことを確認しています。
flex とbison を使って電卓を組んでみましょう。
まずは、整数の足し算と引き算だけ定義します。
パーサ(bison)の定義ファイルです。文法のエッセンスはstatementのところだけです。雰囲気はわかると思います。
%{ #include <stddef.h> #include <stdarg.h> #include "yacc_port.h" #include "fopen.h" #include "printy.h" extern int yylex(void); extern void yyerror(const char*msg); %} %union { int dec_val; } %token <dec_val> DEC_VAL %% statement : DEC_VAL '+' DEC_VAL { printf("YACC Answer:%d+%d=%d\n",$1,$3,$1+$3); } | DEC_VAL '-' DEC_VAL { printf("YACC Answer:=%d-%d=%d\n",$1,$3,$1-$3); } ; %% void compile(const char* path) { if(!reset_lexor(path)){ printf("can not open %s",path); return ; } int rc = yyparse(); }
スキャナ(flex)の定義ファイルです。[0-9]+ で整数を切り出しています。flex
が生成するファイルは、stdio.h をどうしても含んでしまいます。そこで、生成されたlexor.c
は、YACC用に数行書き換えます。
//#include <stdio.h> //#include <errno.h> #include <limits.h> #include <ctype.h> #include <stdarg.h> #include <string.h> #include "yacc_port.h" #include "printy.h" #include "malloc.h" #include "fopen.h"
%{ unsigned line_no=1; #include "parse.h" void yyerror(const char *); #define YY_NEVER_INTERACTIVE 1 void yy_fatal_error_yacc( const char* msg ); #define YY_INPUT(buf,result,max_size) result=fread(buf, 1, max_size, yyin) #define YY_FATAL_ERROR(msg) yy_fatal_error_yacc( msg ) %} %% [0-9]+ { yylval.dec_val=atoi(yytext); return DEC_VAL; } [}{;:\[\],()#=.@&!?<>%|^~+*/-] { return yytext[0]; } \n { line_no++; } . { } %% int yywrap() { return 1; } void yy_fatal_error_yacc( const char* msg ) { printf("Error:%s\n",msg); } void yyerror(const char *msg) { printf("Error:%s (%d)\n",msg,line_no); } int reset_lexor(const char * path) { yyin = fopen(path, "r"); if (yyin) return 1; else return 0; }
後は、メインからパーサを呼ぶだけです。
char name[]="calc_test1.txt"; void main() { compile(name); print("$finish"); }
テキストファイルから計算式を読み取り計算結果を出力します。
テキストファイルは
9871+12342121
です。
ちゃんと計算してくれました。それでは、この電卓を拡張していきましょう。
まずは、乗除算定義を追加しましょう。パーサだけの変更で済みます。%leftの定義順に演算優先度が上がります
%token <dec_val> DEC_VAL %type <dec_val> expr primary %left '+' '-' %left '*' '/' '%' %% statement : expr { printf(" YACC Answer=%d",$1);} ; expr: primary { $$=$1; } | expr '+' expr { $$=$1 +$3; } | expr '-' expr { $$=$1 -$3; } | expr '*' expr { $$=$1*$3;} | expr '/' expr { $$=$1/$3;} ; primary: DEC_VAL { $$=$1;} ; %%
計算式の入力ファイルです。
結果です。
もう少し拡張してみましょう。括弧をつけたくなりました。これもパーサに僅かな追加で済みます
%token <dec_val> DEC_VAL %type <dec_val> expr primary %left '+' '-' %left '*' '/' '%' %% statement : expr { printf(" YACC Answer=%d",$1);} ; expr: primary { $$=$1; } | expr '+' expr { $$=$1 +$3; } | expr '-' expr { $$=$1 -$3; } | expr '*' expr { $$=$1*$3;} | expr '/' expr { $$=$1/$3;} ; primary: DEC_VAL { $$=$1;} | '(' expr ')' { $$=$2; } ;
すると以下の計算もしてくれるようになります。
次は、いくつかの計算をまとめて行いたいとします。これはステートメントが複数になったと考えます。そのためには、ステートメントの区切りが必要になります。ここでは、区切りを=としました。これも以下のように僅かなパーサ追加だけで出来ます。
%union { int dec_val; } %token <dec_val> DEC_VAL %type <dec_val> expr primary %left '+' '-' %left '*' '/' '%' %% statements : statement | statements statement ; statement : expr '=' { printf(" YACC Answer=%d\n",$1);} ; expr: primary { $$=$1; } | expr '+' expr { $$=$1 +$3; } | expr '-' expr { $$=$1 -$3; } | expr '*' expr { $$=$1*$3;} | expr '/' expr { $$=$1/$3;} ; primary: DEC_VAL { $$=$1;} | '(' expr ')' { $$=$2; } ; %%
入力ファイルです。
・超簡単インタープリタ
上の電卓の延長で設計してみましょう。超簡単仕様です。
限りなく電卓に近いです。これなら30分で出来ますね。
まずは、仕様の分析です。
といったことが浮かびます。あとは、パーサとレキサ(スキャナ)をちょっといじればよいだけです。
パーサです。
#define VAR_MAX 256 int var[VAR_MAX]; %} %union { int dec_val; unsigned char ident; } %token <dec_val> DEC_VAL %token <ident > IDENTIFIER %token PRINT_K %type <dec_val> expr primary %left '+' '-' %left '*' '/' '%' %% statements : statement | statements statement ; statement : IDENTIFIER '=' expr ';' { var[$1-'a']=$3; } | PRINT_K expr ';' { printf("YACC Answer=%d\n",$2); } ; expr: primary { $$=$1; } | expr '+' expr { $$=$1 +$3; } | expr '-' expr { $$=$1 -$3; } | expr '*' expr { $$=$1*$3;} | expr '/' expr { $$=$1/$3;} ; primary: DEC_VAL { $$=$1;} | IDENTIFIER { $$=var[$1-'a']; } | '(' expr ')' { $$=$2; } ; %% void compile(const char* path) { int i; if(!reset_lexor(path)){ printf("can not open %s",path); return ; } for (i=0;i<VAR_MAX;i++) var[i]=0; int rc = yyparse(); }
レキサです。
%% [0-9]+ { yylval.dec_val=atoi(yytext); return DEC_VAL; } [}{;:\[\],()#=.@&!?<>%|^~+*/-] { return yytext[0]; } [a-z] { yylval.ident=yytext[0]; return IDENTIFIER;} "print" { return PRINT_K;} \n { line_no++; } . { } %%
インタープリタの入力です
結果です。
パーサとレキサについてみました。簡単な文法ならBison/Flexだけでパーサの殆どができてしまいます。事実、Verilog1995については、殆どこれで書けています。パースは、全体を舐めるという感じでしょうか? 今回は、超簡単インタープリタなので、パーサだけで全てが書けました。これより大きい規模では、意味解析、Elaboration,中間コード生成、コード生成というステージが後続します。インタープリタというのは、コンパイラが出来て初めて作ることができます。コンパイラが出来ないうちにインタープリタを作ることはできません。
この乗りで、自分専用のスクリプト言語を書いてみるのも面白いかもしれません。
http://www.geocities.jp/team_zero_three/ のsrc2html から、有難くソースをいただいてきました。Cソースをhtmlにしてくれます。ファイル関係はバイナリオープンしか実装していないので、少しソースを修正して対応しました。RTLシミュレータが生成した修正後のhtmlソースです。
ついでにVerilogHDLも色つきにしてみましょう。RTLシミュレータが生成した新しいYACCのCPUコア部ソースです。(最新ではありません。) この出力には
11分かかりました。YACCも自分のソースを自分で出力してくれるようになって感慨深いです。(まだシミュレータ上です)
http://www.sweetcafe.jp/ サイトから、cjpeg.c というファイルを頂いてきました。これは、ビットマップをjpegに変換するソフトです。(ご自身でverilogによるjpegコアを公開されています。)このソースは、構造体アクセス、浮動小数・整数各種演算が織り交ぜてあり、CPUとキャッシュのチェックをするには、丁度よいです。CPUシミュレータでは数分、Veritak高速モードで2-3時間回すとやっと小さいBMPファイルが変換されてJPEGファイルが出来上がりました。CPUシミュレータが吐いたログファイルは5GBでしたので、さすがにコンペアする気にはなれません。CPUシミュレータのJPEGファイルとRTLシミュレータのJPEGファイルをバイナリコンペアをとってOKしました。
テストに使ったビットマップファイルです。どこか見覚えがありますね。
Veritakが吐いたJPEGファイルです。
所感
CPUとキャッシュの検証を行うのに、適当なCを動かしたかったのですが、やはりOSが動く前にCを動かすにはstdio.h周りを搭載しないと動いてくれないものが大半であり、そのためのライブラリ作成に時間を取られました。多分ハードを書いている時間よりも長かったと思います。ブートストラップはやっぱりCから始まることを実感しました。マクロ定義の海、可読性の悪いコード、人はそれを中世の呪文というのかもしれません。しかし、作業をやりながら、それなしには、C++もJAVA、その他、巷に溢れる言語は存在しなかった ということを身にしみて感じました。昔の人は偉かったのですね。
論理合成
ブロックの合成(CPUコア単体のみ)
RTL シミュレーションが大体終わったので、試しにCPU単体のみVirtex5で合成してみました。(それにしても合成は、遅いですね)
速度は、8xMHzでした。Stratix2でも6xMHzなので、予想以上に悪いできです。旧版YACCでStratix2で165MHzでしたから、フル32ビットという条件を割り引いても、やはり遅延パスを最初から意識した設計とそうでないものの差は、大きいことが分かります。
遅延のネックは、ジャンプにかかわっています。旧版YACCでは、DelayedSLOTを3とした理由がここにあります。つまりジャンプ命令を犠牲にして速度を上げていたのです。合成前の同じ周波数で比較すると新しいYACCの方がMIPS値は10%-20%程度高くなるのですが、最大駆動周波数がこのような状況なので、合成後では、旧版YACCの方が結果的には速くなります。
Cyclone、Spartan3でも合成してみましたが、4xMHzでした。(Stratix3でもやってみたのですが、Quartus(6.1)のネットリストの書き込みでInternalErrorとなってしまい、遅延SIMで動作確認ができなかったので割愛します。)
Quartusで遅延パスの解析をしてみると、予想通りの箇所がネックになっているようです。やはりCPU単体では100MHz位は出て欲しいので、対策改善について、後で考えることにしましょう。とりあえず、キャッシュなしのCPU単体で、記述上の問題等ないかどうかを調べるために、遅延シミュレーションを行い動作確認を行いました。デバイスは、上記デバイス、VeritakVersionは3.32(以上でないと動きません)です。
遅延SIMとRTLで、基本的には、合成対象だけ置き換えればよいのですが、若干気をつけるところがあります。
ブロックを合成ブロックに置き換えての注意点
今回の遅延SIMは、CPUコア単体だけ合成しています。Wishboneバスコントローラ、Iキャッシュ、Dキャッシュは入っていません。つまりCPUは、合成器が生成したモジュールで、その他は、RTLモデルをそのまま使っています。ただし、以下の点で少し変更しています。
クロック周期
合成器が言ってきた値でよいような気がしますが、最大駆動周波数と、入力最大遅延は、必ずしも一致しません。同期系としての最大駆動周波数とは別にFPGAの入力遅延が入ってきます。こちらのパス遅延の方が大きいと今までのRTLモデリングの位相関係がずれることになってしまうので、今までのRTLモデルをそのまま接続することができなくなってしまいます。今回は、単純は記述関係のチェックが目的なので大きなクロック周期を入力して同じテストベンチを使用しています。
Z問題
RTLでは、Zでfloating だった信号を入力しても問題なかった信号でも、合成モジュールでは、かならずIOバッファが入ってくることに注意します。これらは、Z信号を受け取るとその出力はXになってしまいRTLとの挙動が一致しなくなります。従って、合成モジュールには、フローティング信号を入れないように初期化して入力するようにします。これは、RTL記述から気をつけたいです。
レース問題
他のRTLモジュールは、Zero遅延の同期系にたいして、合成モジュールは、CLK,データパス独立に一本一本遅延が異なります。つまりCLKの方が早く到着すれば問題ないのですが、そうなる保証はどこにもありません。ここでの問題はCLKの方が遅く到着した場合は、レースになってしまうことです。このことは制御のしようがないので、テストベンチ側であらかじめDataPathに対して、十分大きな遅延を持たせてレースが発生しないようにしてやります
以上の3つの点に注意して、ifdefで、RTLと遅延SIMを切り替えるようなベンチとしました。
Virtex4/Spartan3/Stratix2/Cycloneでも同様に遅延SiMを行い、簡単な動作確認を行っています。とりあえず、CPU単体については記述上の問題はなさそうです。(今回は、Xilinx用・Altera用と記述をifdefで分けていません。)
このようにブロックで合成ー遅延SIMでボトムアップしていく方法は、いきなり全ブロックを動かして、動かない->解析よりも効率がよいと思います。
RTLの場合でしたら、全内部信号を殆ど即座に見れますが、合成後になるとRTL内部信号は原型をとどめていないので、動かなかったときの解析に非常に手間取ります。また、これくらいの規模でもCPU単体での合成、PostLayoutで1時間位待たされます(Xilinxの場合)ので、願わくば確認で終わり、ここでデバッグはしたくないものです。
全体の合成
全体の合成をしてみました。デバイスはStaratix2です。全体の合成では、CPUコアの外側にあった仮想RAMと仮想IOポートをはずし、FPGAの外側に移動させます。これは、仮想的IOポートになるのでバスを横取りする必要はなくforce/releaseは外しています。
下位のブロックを動かすのに仮想デバイス(ベンチ)を使い、そのブロックの動作を確認したら、またさらに上位に仮想デバイスを持って動作させボトムアップしていきます。実際のSDRAMコントローラや、232Cポートがなくても、仮想的なデバイスを仕立て動作させることができます。
今回は、CPUコア、Iキャッシュ、Dキャッシュ、WishboneバスコントローラをFPGA上のH/Wとしています。ハード的には未だ未完成ですが、バス上で動作するCPUの検証が当面の目的ですのでこれでよいのです。
WishboneのSlaveは、二つで仮想RAM、仮想IOポートです。FPGAの外側で駆動するものとします。
下は、Stratix2による合成結果です。(Dキャッシュ16KB ,Iキャッシュ16KB)
駆動周波数は、CPU単体よりもさらに下がり54MHzになってしまいました。キャッシュ周りの遅延がプラスされているのだと思います。
AlteraでRAMを推定させるための記述
今回は、意識してWizardでRAMを記述していません。できるならXilinx/Alteraとも同じソースにしたいからですが、当初の記述では、RAMをinferしてくれませんでした。(Quartus7.0) タグRAMは本質的にDualです。WriteとRead同じタイミングで、違うアドレスを処理するためにDUAL動作が必須になります。
同じアドレスなので冗長のような気がしますが、こうしないとFFとして合成され膨大なリソースが必要になってしまいます。多分Readに関しては、明示的な同期FFが合成器にとっては必要なのでしょう。それを明示してやることで、RAMが推定されやすくなったのだと思います。Xilinxではこの部分はなくても2ポートRAMを推定してくれるようですが、2ポートRAMに関しては、また別の問題があります。(後述)
//Mar.26.2007 for synthesis reg [RAM_ADDRESS_MSB:0] addr_way_temp; reg [RAM_ADDRESS_MSB:0] addr_way_reg,addr_way_reg_latch;//Mar.27.2007 always @(*) begin addr_way_temp=!pipe_proceed ? addr_way_reg : virtual_address[4 +:RAM_ADDRESS_MSB+1]; end always @(posedge clock) begin begin addr_way_reg<=addr_way_temp;// addr_way0<= addr_way_temp;//Mar.28.2007 for Quartus:write 用とRead用を分けることでDual Port RAMを認識させる addr_way1<= addr_way_temp;//Mar.28.2007 for Quartus:write 用とRead用を分けることでDual Port RAMを認識させる addr_control<=addr_way_temp;//Mar.28.2007 for Quartus:write 用とRead用を分けることでDual Port RAMを認識させる end end
遅延SIMです。駆動周波数(25MHz)
Xilinx Dual Port RAM の問題
Alteralでは、Megawizardを使わずに遅延SIMできたので、Xilinxでも同じ記述で行けるだろうと思いたいのは人情というものですが、残念ながらそう簡単ではありません。XilinxのDual PortRAM は、WriteポートとReadポートが同じアドレスでの書き込みだとReadが不定になる(Write
First)という仕様です。これですとRTLの振る舞いと合成後の振る舞いが異なることになってRTLシミュレーションが通っても遅延SIMが通らないということになってしまいます。Coregenを使ってRAMを生成させれば、RTLレベルでも警告を発しますが、Wizardを使わない記述では、遅延SIMをして、初めておかしいことに気づきます。
この問題は、分散RAMを使えば回避することができます。事実、32bitx32bitのレジスタファイルは、次のように記述して、分散RAMを指定しています。
(ちなみに `ifdef で、// synthesis.. を無視させたりすることはできないようです。)
//May.23.2005 //Tak.Sugawara //Generic RAM module // module gen_ram32x32 ( input wire [31:0] data, input wire wren,//wr input [4:0] wraddress, input [4:0] rdaddress_a,rdaddress_b, input wire clock, output wire [31:0] qa, qb, input wire stall,sync_reset); reg [4:0] address_a_reg,address_b_reg;//address register // synthesis attribute ram_style of mem is distributed; reg [31:0] mem [0:31];//actual memory integer i; //address_a_reg always @ (posedge clock) begin if(sync_reset) address_a_reg<=0; else if (!stall) address_a_reg<=rdaddress_a;//stall のときは、前の状態を維持 end //adress_b_reg always @ (posedge clock) begin if (sync_reset) address_b_reg<=0; else if (!stall) address_b_reg<=rdaddress_b;//stall のときは、前の状態を維持 end //read operation assign qa= mem[address_a_reg]; assign qb= mem[address_b_reg]; //stall 処理 //write operatiion always @(posedge clock) begin if (wren && !stall ) //$Z0 には書かない仕様 mem[wraddress]<=data; end initial begin //Note: YACC needs 0 cleared register at power on for (i=0;i<32;i=i+1) begin mem[i]=0;//FPGA 初期値0を期待しHard的な初期化回路を用意していない end end `ifdef RTL_SIM reg [5*8:1] ra_name="abcd",rb_name="abcd" ,wr_name="abcd"; always @* begin ra_name=get_reg_name(rdaddress_a); rb_name=get_reg_name(rdaddress_b); wr_name=get_reg_name(wraddress); end function [4*8:1] get_reg_name; input [4:0] field; begin case (field) 0: get_reg_name="$z0"; 1: get_reg_name="$at"; 2: get_reg_name="$v0"; 3: get_reg_name="$v1"; 4: get_reg_name="$a0"; 5: get_reg_name="$a1"; 6: get_reg_name="$a2"; 7: get_reg_name="$a3"; 8,9,10,11,12,13,14,15: $sprintf(get_reg_name,"$t%1d",field-8); 16,17,18,19,20,21,22,23,24,25: $sprintf(get_reg_name,"$s%1d",field-16); 26:get_reg_name="$k0"; 27:get_reg_name="$k1"; 28:get_reg_name="$gp"; 29:get_reg_name="$sp"; 30:get_reg_name="$s8"; 31:get_reg_name="$ra"; endcase end endfunction `endif endmodule
しかし、この手が使えるのは、この程度の小さい規模までで、10Kbit以上あるキャッシュのタグRAMには適用することができません。(試しにタグRAMを分散RAMで合成してみると、合成は終わりましたがレイアウトが終わらない気配でした。)
タグRAMは、仕様上、どうしても真のDual動作である必要があるため回路的な対策をしてあります。(前回ライトで同じアドレスならRAMの出力ではなく、別にSaveしたレジスタ出力をReadに見せる) しかし、遅延SIMでは、defaultでこのWarningがオンになりコンソールがWarningで埋まってしまい大事なログが見れなくなってしまいます。
仕方ないので、最初、Primitive Libraryを次のように変更して無用なWarningを抑制しています。
(Note:Virtex5、Spartan3Aの合成では、defparam で、オーバライドされてしまうので、下記の変更は意味がありません。仕方ないので、NET LIST(*timesim.v)
defparam \yacc_as_wb_master/i_cache_module/ram3_way0/Mram_mem .SIM_COLLISION_CHECK
= "NONE";とういう風に全置換しました。)
Veritak 遅延SIMのプロジェクト構成です。
spartan3s1500で全体を合成してみました。駆動可能周波数は30MHz位です。
遅延SIM結果です。駆動周期(42ns spartan3Eは、48ns)
Virtext5でも合成してみました。
同様に、Virtex5,Spartan3A, Spartan3E, Virtex4,Cyclone3(Quartus 7.0) でも合成・遅延SIMを行いました。
Wizardを使わないので、余計なチェックは入らずRTL SIMは快速ですが、重要な警告を見落とす可能性もあるので、Wizardを使った方がよいかもしれません。慣れたら、今回の方法でも (Altera/Xilinxを`ifdef
で切り替える必要もない)よいかもしれません。
駆動可能周波数(ポストレイアウト Clock周波数)
Staratix3 | Quartus Error (7.0) |
Virtex4 | 55MHz |
Virtex5 | 50MHz |
Stratix2 | 54MHz |
Cyclone3 | 38MHz |
Startan3/3A | 30MHz |
割り込み関係のRefineでまた少し変わりますが大体上記の駆動周波数になると思います。 チューニング前の値としては、こんなものでしょうか。
仕様の説明
1.バイトアドレシング
ビッグエンディアンのみです。
アクセスは、バイトアクセス、ワードアクセス(2バイト)、ロングアドレス(4バイト)があります。
Slave RAM ポートアクセスは、全てLongになります。これは、全てキャッシュを介してアクセスしているからです。IO領域でのアクセスは、下表のようになります。Readについては、Long/Word/Byteに係わらず、全てのByteEnableがEnableされます。(従って、外部側では、アクセス単位は分かりません。) Writeについては、それぞれ対応するバイトがEnableされます。
Access(n:address) | wb_sel_i | |||||||
Read | [3] | [2] | [1] | [0] | ||||
Long | 1 | 1 | 1 | 1 | ||||
Word | 1 | 1 | 1 | 1 | ||||
Byte | 1 | 1 | 1 | 1 | ||||
Write | ||||||||
Long | 1 | 1 | 1 | 1 | ||||
Word n%4==0 | 1 | 1 | ||||||
Word else if(n%2==0) | 1 | 1 | ||||||
Byte n%4==0 | 1 | |||||||
Byte (n%4==1) | 1 | |||||||
Byte (n%4==2) | 1 | |||||||
Byte (n%4==3) | 1 |
2.キャッシュ
インストラクション:非キャッシュ領域指定:ありません。プログラムアクセスは全てキャッシュされます。
データキャッシュ: 非キャッシュ領域指定:あります。指定領域は、キャッシュされません。
3.リセット
リセット例外はありません。ハードリセットで、リセットベクタからスタートします。キャッシュのゼロ初期化が必要になりますが、これは、FPGAのハード初期化を前提にしています。
4.コンパイルオプション
以下が可能です。
HDLファイルのdefine.h で指定します。
//Compile Options `define IO_SPACE 16'h001f //001f_xxxx 64KB space None-Decached Domain `define Reset_Vector 0 //Reset Vector `define RESET_ADDRSS `Reset_Vector `define UTLB_Exception 32'h0000_0080 //Reserved `define General_Exception 32'h0000_0080 //Interrupt Vector `define ID_CACHE_SIZE (512) //512->16KBytes for each Icache/Dcache Min.=8,16,32....limitted by Vavlue of IO_SPACE
上記IO_SPACEで、Dataの非キャッシュ領域を指定します。本プロジェクトでは、とりあえず、2MBの外部メモリを想定して、最上位64KBをIO領域としてします。IO_SPACEが使われるのは、Dキャッシュの中だけで、次のコードです。
wire non_cache_domain1=virtual_address[31:16]==`IO_SPACE;// wire non_cache_domain=non_cache_domain1 && pipe_proceed;//
5.割り込み
独自の実装です。HDLで定義される割り込みVectorに分岐します。
6.カーネルモード・ユーザモード
カーネルモードで動作します。(特に実装していません。)
使用方法の説明
1.デザイン階層
下図の通りです。DUT以下がハードウェア合成対象です。virtual_io_portとonnchip_ramは、Wishboneバスに接続されたFPGA外部のベンチ(仮想)です。合成対象のDUTは、やはりWishboneバスに接続されるので、FPGAのIFの殆どは、下図の通り、Wishboneバスの為の線になります。
2.環境のインストール
解凍すると以下のフォルダになっています。
アイテム | 所在 |
テスト用Cソース | malloc2 |
Cソースコンパイルの為のバッチファイル | malloc2 |
Cコンパイラ | mips-elf |
HDLソース | rtl |
ツールソース/VC++7プロジェクトファイル | tool_source_by_tak |
Veritakプロジェクトファイル | rtl |
3.Cコンパイラを使う
malloc2フォルダで、compilexx.bat がCコンパイルするバッチファイルです。例えばcompile.batは、次のようになっています。
手順としては、スタートアップファイルplasmaboot.asm を呼んで、当該cソースをmipc-elf-gccでコンパイルします。Cコンパイルは、すべて、-msoft-floatをつけてください。 その後、リンカmips-elf-ldで、オブジェクトファイルをリンクしてtest.exeという実行形式のファイルを生成します。test.exeを逆アセンブルしたファイルがlist1.txtとして生成されます。また、マップ情報が、test.mapとして出力されます。test.map情報を入力とするツールconvert_mips3.exeで、スタックポインタ(ここでは、1efff0 16バイトアラインとしてください。)の細工をして、読み込むファイルが出来上がります。出力は、各種形式で出力していますが、HDLシミュレーションでは、Veritakの$hex2veriで、hexファイルをVerilog形式にしています。yams.exe
は、cpuシミュレータで、リファレンスとしてのシミュレーションを行います。結果は、disasm.txtとして出力されます。(2-3分で数百MB生成しますので、気をつけてください。)
path; path=..\..\..\mips-elf\mips-elf-yacc\bin mips-elf-as -o boot.o plasmaboot.asm mips-elf-gcc -msoft-float -O2 -O -DRTL_SIM -Dmain=main2 -Wall -c count_tak.c mips-elf-ld.exe -Ttext 0 -eentry -Map test.map -s -N -o test.exe boot.o count_tak.o mips-elf-objdump.exe --disassemble test.exe > list.txt rem =must be 16byte alignment convert_mips3.exe 1efff0 copy *.hex ..\..\..\rtl\altera\*.* copy *.hex ..\..\..\rtl\generic\*.* copy *.coe ..\..\..\rtl\xilinx\*.* copy *.mif ..\..\..\rtl\xilinx\*.* copy *.hex ..\..\..\rtl\*.* yams.exe test.exe test.map 200000 1efff0
スタートアップファイルの中身
Cメインに飛ぶまでの十数行は、convert_mips3.exeが設定しますので、変更しないでください。$gpと$spは、convert_mips3.exeが自動設定します。
################################################################## # TITLE: Boot Up Code # AUTHOR: Steve Rhoads (rhoadss@yahoo.com) # DATE CREATED: 1/12/02 # FILENAME: boot.asm # PROJECT: Plasma CPU core # COPYRIGHT: Software placed into the public domain by the author. # Software 'as is' without warranty. Author liable for nothing. # DESCRIPTION: # Initializes the stack pointer and jumps to main2(). ################################################################## .text .align 2 .globl entry .ent entry entry: .set noreorder #These eight instructions must be the first instructions. #convert_mips3.exe will correctly initialize $gp lui $gp,0 ori $gp,$gp,0 #convert.exe will set $4=.sbss_start $5=.bss_end lui $4,0 ori $4,$4,0 lui $5,0 ori $5,$5,0 lui $sp,0 ori $sp,$sp,0xfff0 #initialize stack pointer $BSS_CLEAR: sw $0,0($4) slt $3,$4,$5 bnez $3,$BSS_CLEAR addiu $4,$4,4 sw $5,bss_end_save # lui $4,0 # ori $4,0xff01 #現状態 カーネルモード、割り込みEnable/割り込みEnableビットSET # mtc0 $4,$12 #割り込みEnable ,CPUシミュレータが割り込みをサポートしていないのでカット jal main2 # C メインに飛ぶ nop $L1: j $L1 .align 4 .globl bss_end_save bss_end_save: .long 0 .org 0x80 #.set noreorder #address 0x3c interrupt_service_routine:#インタラプトハンドラ addi $sp, $sp, -25*4 sw $1, 1*4($sp) sw $2, 2*4($sp) sw $3, 3*4($sp) sw $4, 4*4($sp) sw $5, 5*4($sp) sw $6, 6*4($sp) sw $7, 7*4($sp) sw $8, 8*4($sp) sw $9, 9*4($sp) sw $10, 10*4($sp) sw $11, 11*4($sp) sw $12, 12*4($sp) sw $13, 13*4($sp) sw $14, 14*4($sp) sw $15, 15*4($sp) sw $24, 16*4($sp) sw $25, 17*4($sp) sw $28, 18*4($sp) sw $30, 19*4($sp) sw $31, 20*4($sp) #ここで C 割り込みユーザルーチンを呼ぶ lw $1, 1*4($sp) lw $2, 2*4($sp) lw $3, 3*4($sp) lw $4, 4*4($sp) lw $5, 5*4($sp) lw $6, 6*4($sp) lw $7, 7*4($sp) lw $8, 8*4($sp) lw $9, 9*4($sp) lw $10, 10*4($sp) lw $11, 11*4($sp) lw $12, 12*4($sp) lw $13, 13*4($sp) lw $14, 14*4($sp) lw $15, 15*4($sp) lw $24, 16*4($sp) lw $25, 17*4($sp) lw $28, 18*4($sp) lw $30, 19*4($sp) lw $31, 20*4($sp) addi $sp, $sp, 25*4 mfc0 $27,$14 # EPC=14 戻り番地が記録してあるEPCを$27に読む jr $27 # 割り込み復帰 rfe # 割り込みEnable、ユーザモード復帰 nop .set reorder .end entry
命令パイプラインの様子です。ストールがないとき、命令は、1命令/1CLKで処理されていることが分かります。
Dcacheの様子です。ドライストーンベンチで、
`define ID_CACHE_SIZE (8*16) //512->16KBytes for each Icache/Dcache Min.=8,16,32....limitted by Vavlue of IO_SPACE と意図的にキャッシュ容量を少なくしてキャプチャしています。default設定の512(=16KB Dcache)では、全部キャッシュに収まって、ライトバックシーケンスを見ることはできません。 シーケンスは、Dcacheにヒットしなかったときにstall_to_cpuをアサートしてCPUに待ちを指示します。その間、ライトバックでメインメモリへの書き戻し、要求ラインをリフィルします。 ext_writeは、Wishboneバスへのライトストローブ、ack_fm_extがWishboneバスからのAckです。
4.割り込みルーチンをCで記述する
4.1 スタートアップファイル
専用のスタートアップファイル(以下)を使ってください。上とほぼ同じですが、インタラプトをEnableして、割り込みルーチンでは、コンテキストのSave/RestoreとCルーチンの呼び出しを行います。コンテキストは、全32レジスタをSaveする必要はなく以下で十分です。多重割り込みには(ファームが)対応していません。
################################################################## # TITLE: Boot Up Code # AUTHOR: Steve Rhoads (rhoadss@yahoo.com) # DATE CREATED: 1/12/02 # FILENAME: boot.asm # PROJECT: Plasma CPU core # COPYRIGHT: Software placed into the public domain by the author. # Software 'as is' without warranty. Author liable for nothing. # DESCRIPTION: # Initializes the stack pointer and jumps to main2(). ################################################################## .text .align 2 .globl entry .ent entry entry: .set noreorder #These eight instructions must be the first instructions. #convert.exe will correctly initialize $gp lui $gp,0 ori $gp,$gp,0 #convert.exe will set $4=.sbss_start $5=.bss_end lui $4,0 ori $4,$4,0 lui $5,0 ori $5,$5,0 lui $sp,0 ori $sp,$sp,0xfff0 #initialize stack pointer $BSS_CLEAR: sw $0,0($4) slt $3,$4,$5 bnez $3,$BSS_CLEAR addiu $4,$4,4 sw $5,bss_end_save lui $4,0 ori $4,0xff01 #現状態 カーネルモード、割り込みEnable/割り込みEnableビットSET mtc0 $4,$12 #割り込みEnable jal main2 # C メインに飛ぶ nop $L1: j $L1 .align 4 .globl bss_end_save bss_end_save: .long 0 .org 0x80 #.set noreorder #address 0x3c interrupt_service_routine:#インタラプトハンドラ addi $sp, $sp, -25*4 sw $1, 1*4($sp) sw $2, 2*4($sp) sw $3, 3*4($sp) sw $4, 4*4($sp) sw $5, 5*4($sp) sw $6, 6*4($sp) sw $7, 7*4($sp) sw $8, 8*4($sp) sw $9, 9*4($sp) sw $10, 10*4($sp) sw $11, 11*4($sp) sw $12, 12*4($sp) sw $13, 13*4($sp) sw $14, 14*4($sp) sw $15, 15*4($sp) sw $24, 16*4($sp) sw $25, 17*4($sp) sw $28, 18*4($sp) sw $30, 19*4($sp) sw $31, 20*4($sp) #ここで C 割り込みルーチンを呼ぶ jal interrupt # C メインに飛ぶ lw $1, 1*4($sp) lw $2, 2*4($sp) lw $3, 3*4($sp) lw $4, 4*4($sp) lw $5, 5*4($sp) lw $6, 6*4($sp) lw $7, 7*4($sp) lw $8, 8*4($sp) lw $9, 9*4($sp) lw $10, 10*4($sp) lw $11, 11*4($sp) lw $12, 12*4($sp) lw $13, 13*4($sp) lw $14, 14*4($sp) lw $15, 15*4($sp) lw $24, 16*4($sp) lw $25, 17*4($sp) lw $28, 18*4($sp) lw $30, 19*4($sp) lw $31, 20*4($sp) addi $sp, $sp, 25*4 mfc0 $27,$14 # EPC=14 戻り番地が記録してあるEPCを$27に読む jr $27 # 割り込み復帰 rfe # 割り込みEnable、ユーザモード復帰 nop .set reorder .end entry
割り込み記述、サンプルCソースです。
#include "yacc_port.h" #include "printy.h" #include "malloc.h" void main() { unsigned long i; for (i=0;i<1000;i++){ unsigned c='*'; printf("%c",c); } print("$finish"); } unsigned counter=0; void interrupt() { printf("\nInterrupt Handler :counter=%d\n",counter++); }
compile_interrupt.batでコンパイル後、シミュレーションすると以下のようになります。
4.2コプロセッサ関係の仕様
割り込み関係は、コプロセッサの仕事で独自の実装です。言葉で説明するよりも、HDLコードを見たほうが早いです。
なお、割り込みは、レベルセンスでactive Hです。割り込みを認識すると新たな割り込みは、次の記述でマスクされます。
//割り込み検出 wire [7:0] ints={ IP,Sw} & IntMask & {8{IEc}};
IEcビットは、割り込み検出と復帰命令で変化します。
なお、割り込みハンドラを抜けるまえに、割り込み端子をLに戻してください。そうしないと1命令だけ実行後、再度割り込みがかかる仕様です。
//コプロセッサ------------------------------------------------------------------------------------ // レジスタ reg [31:0] EPC;//割り込み退避アドレス reg KUo,IEo,KUp,IEp,KUc,IEc; reg [7:0] IntMask; reg BD;//例外処理時にディレイスロット reg [5:0] IP;//外部インタラプト 処理中 reg [1:0] Sw;//ソフトインタラプト 処理中 reg [3:0] ExeCode;//例外要因 //コプロセッサ内部信号 //割り込み reg [7:0] int_old;//割り込み検出マシン wire int_rise; wire int_recog; wire [4:0] copro_read_address=IR_LR[15:11];//MFC0 rd wire [4:0] copro_write_address=IR_A[15:11];//MTC0 rd wire [31:0] status_reg={16'h0,IntMask,2'b00, KUo,IEo,KUp,IEp,KUc,IEc}; wire [31:0] cause_reg={BD,15'h0,IP,Sw,2'b00,ExeCode,2'b00}; wire [31:0] copro_output=copro_read_address==Status_Address ? status_reg : copro_read_address==Cause_Address ? cause_reg :EPC; wire cop_write=!dstall && IR_A[31:26]==COP0 && IR_A[25:21]==MT; //割り込み検出 wire [7:0] ints={ IP,Sw} & IntMask & {8{IEc}}; always @(posedge clock) begin if (sync_reset) int_old<=8'h0; else if (rfe_command) int_old<=8'h0;//Apr.22.2007 割り込みRETで解除 else if (int_recog) int_old<=ints; //割り込みを認識したら取り込み end assign int_rise=( ints & int_old ) ==0 && ints !=0;//割り込み立ち上がり検出、新たな割り込みが認識されるまでHになる assign int_recog=int_rise && !dstall && !delayed_slot_processing;//割り込み認識 assign exception_command=!dstall && (ir_out[31:26]==SPECIAL && ( ir_out[5:0]==SYSCALL || ir_out[5:0]==BREAK));//TODO SYSCALL/BREAK/exception always @(posedge clock) begin if (sync_reset) IP<=0; else IP<=interrupt;//IPはReadOnly end wire delayed_slot_processing=IR[31:26]==BCOND || IR[31:26]==J || IR[31:26]==JAL || IR[31:26]==BEQ || IR[31:26]==BNE || IR[31:26]==BLEZ || IR[31:26]==BGTZ || (IR [31:26]==SPECIAL && ( IR[5:0]==JR || IR[5:0]==JALR)) ; always @(posedge clock) begin if (sync_reset) EPC<=0; else if (int_recog || exception_command) begin // if (delayed_slot_processing) EPC<=PC-4;//Apr.22.2007PC-4;//DelaySlot中なら分岐命令を指す // else EPC<=PC;//Apr.22.2007 分岐命令は、割り込み禁止にした end else if (copro_write_address==EPC_Address && cop_write) EPC<=rf_input; end wire rfe_command=!dstall && (IR_LR[31:26]==COP0 && IR_LR[25] && IR_LR[4:0]==RFE);// (ir_out[31:26]==COP0 && ir_out[25] && ir_out[4:0]==RFE); //Apr.23.2007割り込み元がクリアされていなくても1命令は実行するように調整した。 //ステータスレジスタ always @(posedge clock) begin if (sync_reset)begin//Apr.22.2007 {KUo,IEo,KUp,IEp,KUc,IEc}<=0; IntMask<=0; end else if (copro_write_address==Status_Address && cop_write) begin//レジスタ書き込みApr.22.2007 IntMask<=rf_input[15:8]; {KUo,IEo,KUp,IEp,KUc,IEc}<=rf_input[5:0]; end else if (int_recog || exception_command) begin {KUo,IEo,KUp,IEp,KUc,IEc}<={KUp,IEp,KUc,IEc,2'b00};//例外処理 end else if (rfe_command) begin {KUo,IEo,KUp,IEp,KUc,IEc}<={KUo,IEo,KUo,IEo,KUp,IEp};//例外処理復元 end end //原因レジスタ localparam [3:0] CAUSE_INT=0, CAUSE_SYS=8, CAUSE_BREAK_POINT=9; always @(posedge clock) begin if (sync_reset) ExeCode<=0; else if (int_recog) ExeCode<=CAUSE_INT; else if (ir_out[31:26]==SPECIAL && ( ir_out[5:0]==SYSCALL)) ExeCode<=CAUSE_SYS; else if (ir_out[31:26]==SPECIAL && ( ir_out[5:0]==SYSCALL)) ExeCode<=CAUSE_BREAK_POINT; end always @(posedge clock) begin if (sync_reset) Sw<=0; else if (copro_write_address==Cause_Address && cop_write) begin Sw<=rf_input[9:8];//原因レジスタは、Swのみ書き込みができる end end //BDビット always @(posedge clock) begin if (sync_reset) BD<=0; else if (int_recog || exception_command) begin if (delayed_slot_processing) BD<=1; else BD<=0; end end
5.シミュレーション
デバッグや、内部信号の観測用のプロジェクトと、高速モードでのシミュレーションの二つのRTLシミュレーションのプロジェクトを用意しています。手順は、
です。ゲートシミュレーションについては、添付していませんが、上の記述を参考にすれば、構成できると思います。
6.拡張性
Wishboneバスに複数のマスタ、スレーブを追加できます。つぎの設計としては、シリアルポートの実装がありますが、Wishboneスレーブとして容易に実装できるでしょう。
開発環境一式ダウンロード4/26/2007版 60MB
フォルダrtlの下に配置してください。
Xilinx(Spartan3)/Altera(Cyclone3)プロジェクト14MB 5/9/2007版
仕様の説明用Veritakプロジェクトファイル rtlフォルダに置いて3.35以上で読んでください。(3.35未満で開くと波形中のコメントが消えてしまいます。)
本リリースは、遅延シミュレーションで動かしてはいますが、実機では一切動かしていません。実機で動かすためには、仮想で動かしている外部ROM/RAMモデルや外部GPIOの実装が必要になります。また、VeritakでのUnique機能($hex2ver,$fputc等)を使用しているためにModelSim等では(そのままでは)動きません。
7.CPU単体インターフェース
7.1 設計の考え方
オリジナルを忠実に再現することを目的にはしていません。Cコンパイラを借用してシステムを構築してゆくことができればよし、と考えています。ですので、必要に応じて、OSに近いところは、ファームで対応する必要があります。また、IOや、割り込みは、独自の仕様です。
7.2 オリジナルとの違い
7.2.1サポートしていない命令
実装していません。将来に渡って実装する予定はありません。
これらは、上記パッケージに含まれるコンパイラを使えば生成しません。ただしlibcのmemcpyは、生成してしまうので、パッケージ中、作成したmeemcpyをリンクするようにしてください。
実装していません。将来のVersionでは、サポートする予定ですが、とりあえずなくても支障がないでしょう。
7.2.2 実装がオリジナルとは違う命令
これらは、通常CのCコードでは生成しないと思います。GDBで使うかもしれませんが、未調査です。
以下のコードで割り込みを解除しています。
//割り込み検出 wire [7:0] ints={ IP,Sw} & IntMask & {8{IEc}}; always @(posedge clock) begin if (sync_reset) int_old<=8'h0; else if (rfe_command) int_old<=8'h0;//Apr.22.2007 割り込みRETで解除 else if (int_recog) int_old<=ints; //割り込みを認識したら取り込み end assign int_rise=( ints & int_old ) ==0 && ints !=0;//割り込み立ち上がり検出、新たな割り込みが認識されるまでHになる assign int_recog=int_rise && !dstall && !delayed_slot_processing;//割り込み認識 assign exception_command=!dstall && (ir_out[31:26]==SPECIAL && ( ir_out[5:0]==SYSCALL || ir_out[5:0]==BREAK));//TODO SYSCALL/BREAK/exception always @(posedge clock) begin if (sync_reset) IP<=0; else IP<=interrupt;//IPはReadOnly end wire delayed_slot_processing=IR[31:26]==BCOND || IR[31:26]==J || IR[31:26]==JAL || IR[31:26]==BEQ || IR[31:26]==BNE || IR[31:26]==BLEZ || IR[31:26]==BGTZ || (IR [31:26]==SPECIAL && ( IR[5:0]==JR || IR[5:0]==JALR)) ; always @(posedge clock) begin if (sync_reset) EPC<=0; else if (int_recog || exception_command) begin // if (delayed_slot_processing) EPC<=PC-4;//Apr.22.2007PC-4;//DelaySlot中なら分岐命令を指す // else EPC<=PC;//Apr.22.2007 分岐命令は、割り込み禁止にした end else if (copro_write_address==EPC_Address && cop_write) EPC<=rf_input; end wire rfe_command=!dstall && (IR_LR[31:26]==COP0 && IR_LR[25] && IR_LR[4:0]==RFE);// (ir_out[31:26]==COP0 && ir_out[25] && ir_out[4:0]==RFE); //Apr.23.2007割り込み元がクリアされていなくても1命令は実行するように調整した。
乗除算は内部でストールする仕様です。従って、割り込み中に乗除レジスタをSaveする必要はありません。
7.2.3 MMU関係
MMUは未実装なので、関係するコプロセッサ命令は備えていません。
7.2.4コプロセッサ命令
レジスタ番号12の割り込み関係だけ実装しています。上のコードを参照ください。
7.3 CPUコアインターフェース
7.3.1 構造
以下のツリーでyaccがCPUコア単体です。右ペインのI/O がCPUコア単体の信号インターフェースになります。
右クリックで、ソース行に飛ぶと
以下のCPUコア単体(キャッシュ含まず)のインターフェース信号が見えます。
以下、信号の説明ですが、パイプラインの信号をそのまま出力している関係で、分かりにくいと思います。以下のブロック図を見ながら説明します。
Iキャッシュインターフェース
Iキャッシュ-CPU ストール
データキャッシュWriteアクセス
ライトストローブは、上記ブロック図で、IR_LRのFFから作っています。
データキャッシュReadアクセス
Readストローブは、IR_AのFFから作っています。WriteとReadは、同じステージではないので、キャッシュ側でもパイプラインを持つ必要があります。
そのため、CPU側からのStall指示と、Dキャッシュ側からStall指示、両方向のStallに対応してください。
割り込み端子
レベルセンスで'H'で割り込みです。使い方は、上記コードをご参照ください。
marseeさんのDDRコアを評価・実装してみる。
DDRコアとしては、XilinxのEDK(MCM3)、AlteraのMegaWizardで、個別ベンダ毎に生成させることはできますが、個々のPrimitiveライブラリを使うために、ほぼブラックボックスとして取り扱わなければならず勉強用にはなりません。しかし、幸いにもmarseeさんがDDRコアを公開されています。このコアIPは、商用利用不可ではありますが、RTLの日本語コメント付きで書かれているため、内部動作の理解をしながら動かしてみるにはとてもよいと思います。また、YACCの最終目標は、OS上で動くことにありますが、そのためには、広大なメモリが必要で、DDRコントローラIPの使用は、必須です。
一般的なIPの使用手順としては、
で進みます。5.でOKなら、上のキャッシュコントローラと接続することになりますが、今回は、5まで行うことを目標にします。ちなみに筆者は、DDR自体は、触ったことがなくIPの使用は、初めてです。また、DDRは、タイミング周りが非常に厳しいことが予想されるので、実機チェックは不可避です。今回は、Spartan3Eスタータキット上でYACC CPUコア単品上で動く小さいCプログラムを作りIPを実機評価してみましょう。
DDRコントローラとは、
筆者の考えるDDRコントローラとは、出来る限りSRAMの動作に近いものです。つまり、制御バスとしては、
後は、データバス
があればよいことになります。
言い換えると、面倒なDDRとのコミュニケーションプロトコル、リフレッシュ等を隠蔽してくれて、ユーザから見ると、擬似SRAMインターフェースにしてくれるものです。そうは言っても、使用上の制約は付き物ですし、実際いくつかの制約と上記以外のH/Wインターフェースが存在します。
1.仕様の確認
コントローラIPの仕様については、以下のページで述べられています。
http://marsee101.blog19.fc2.com/blog-entry-261.html
IPのソースは、
http://marsee101.blog19.fc2.com/blog-entry-413.html
からダウンロードできます。このページに書かれているシミュレーション環境の構築方法に従ってシミュレーションしてみました。
このSIMでは、RegressionTestを行いやすいように、オリジナルから書き換えています。テストベンチトップと、ハードウェアトップの二つを次のソースで置き換えてます。
ハードウェアTOP(合成階層TOP)です。
// ハードTOP //marseeさんの module DDRtest(clk, reset, lcd_d, lcd_rs, lcd_rw, lcd_e, sf_ce0, rot_a, rot_b, rot_center, btn_east, btn_west, led, sd_a, sd_dq, sd_ba, sd_ras, sd_cas, sd_we, sd_udm, sd_ldm, sd_udqs, sd_ldqs, sd_cs, sd_cke, sd_ck_n, sd_ck_p, sd_ck_fb); //から改造 Mar.3.2008 Tak.Sugawara //Mar.5.2008 clk100をIFに引き出し追加 //Mar.6.2008 HOST IFをclk_outに変更 //Mar.7.2008 reset 不定バグFix `timescale 1ns / 1ps module ddr_controller( input async_reset,//非同期リセット //以下のHostから来る インターフェース 信号は、全て外部clk=Xtal 50MHz に同期している //HOSTへのOUTPUTは、100MHzPOSに同期 仮想 input clk_fm_host, input [31:0] input_address_fm_host,input_data_fm_host, input read_fm_host,write_fm_host, input [3:0] byte_enable_fm_host,//書き込みバイトEnable output reg [31:0] output_data_fm_controller, output reg fifo_full_fm_controller,//address full または Write Fifo Full output reg read_valid_fm_controller,initialize_end_fm_controller, output clk_out,//Mar.6.2008 //DDR インターフェース output [12:0] sd_a, inout [15:0] sd_dq, output[1:0] sd_ba, output sd_ras, sd_cas, sd_we, sd_udm, sd_ldm, inout sd_udqs, sd_ldqs, output sd_cs, sd_cke, sd_ck_n, sd_ck_p, input sd_ck_fb); `include "./ddr_cont_parameters.vh" wire clk50 ,clk100;//Mar.6.2008 wire logic0, logic1; wire dcm_locked; reg [INTERFACE_DATA_WIDTH-1 : 0] input_data; reg [INTERFACE_MASK_WIDTH-1 : 0] input_mask; reg read_write; reg [USER_INPUT_ADDRESS_WIDTH-1 : 0] input_address; reg reset; reg addr_fifo_wren; reg wrdata_fifo_wren; wire addr_fifo_full; wire wrdata_fifo_full; wire rddata_valid; wire initialize_end; wire [QUANTITY_OF_CLK_OUTPUT-1 : 0] ddr_clk; wire [QUANTITY_OF_CLK_OUTPUT-1 : 0] ddr_clkb; wire ddr_cke; wire [DDR_DQS_DM_WIDTH-1 : 0] ddr_dqs; wire [DDR_DATA_WIDTH-1 : 0] ddr_dq; wire ddr_csb; wire ddr_rasb; wire ddr_casb; wire ddr_web; wire [DDR_DQS_DM_WIDTH-1 : 0] ddr_dm; wire [1:0] ddr_ba; wire [DDR_ADDRESS_WIDTH-1 : 0] ddr_address; wire [15:0] DDR_write_data, DDR_read_data; wire read_ddr_cont; wire [INTERFACE_DATA_WIDTH-1 : 0] output_data; reg read_write_node, addr_fifo_wren_node, wrdata_fifo_wren_node; wire reset_ddr_cont; assign logic0 = 1'b0; assign logic1 = 1'b1; dcm100 dcm100_inst( .CLKIN_IN(clk_fm_host), .RST_IN(logic0), .CLKIN_IBUFG_OUT(), .CLK0_OUT(clk50), .CLK2X_OUT(clk100), .LOCKED_OUT(dcm_locked) ); assign reset_ddr_cont = reset | (~dcm_locked); ddr_sdram_cont ddr_sdram_cont_inst ( .clk_in(clk100), .clk_out(clk_out), .clk_fb(sd_ck_fb), .reset(reset_ddr_cont), .input_data(input_data), .input_mask(input_mask), .read_write(read_write), .output_data(output_data), .input_address(input_address), .addr_fifo_wren(addr_fifo_wren), .wrdata_fifo_wren(wrdata_fifo_wren), .addr_fifo_full(addr_fifo_full), .wrdata_fifo_full(wrdata_fifo_full), .rddata_valid(rddata_valid), .initialize_end(initialize_end), .ddr_clk(ddr_clk), .ddr_clkb(ddr_clkb), .ddr_cke(ddr_cke), .ddr_dqs({sd_udqs, sd_ldqs}), .ddr_dq(sd_dq), .ddr_csb(ddr_csb), .ddr_rasb(ddr_rasb), .ddr_casb(ddr_casb), .ddr_web(ddr_web), .ddr_dm(ddr_dm), .ddr_ba(ddr_ba), .ddr_address(ddr_address) ); //Host に返すラインは、全てclk100 ↑に同期させる always @(*) begin//Mar.6.2008 output_data_fm_controller = output_data; fifo_full_fm_controller =wrdata_fifo_full | addr_fifo_full; read_valid_fm_controller =rddata_valid; output_data_fm_controller =output_data; initialize_end_fm_controller =initialize_end; end //Hostから来るデータは、全てclk100 INPUT SCEW対策の↓に同期させる。Input Scewは、2.5nsまで大丈夫。 always @(negedge clk_out) begin input_mask <= ~byte_enable_fm_host; input_data <= input_data_fm_host; input_address<=input_address_fm_host; read_write <=!write_fm_host; end //Mar.8.2008 always @(*) begin reset=async_reset; end always @(*) begin //Mar.6.2008 negedge clk100,posedge async_reset) begin if (async_reset) begin addr_fifo_wren=0; end else if (!addr_fifo_wren && (write_fm_host || read_fm_host) ) begin addr_fifo_wren=1; end else begin addr_fifo_wren=0; end end //clk_outで同期させるので廃止 Host 50MHzに対しコントローラは、100MHzなので、50MHz1CLK が100MHzでは2CLKになってしまう。その補償回路 always @(*) begin //Mar.6.2008 negedge clk100,posedge async_reset) begin if (async_reset) begin wrdata_fifo_wren=0; end else if (!wrdata_fifo_wren && write_fm_host) begin wrdata_fifo_wren=1; end else begin wrdata_fifo_wren=0; end end assign sd_ck_n = ddr_clkb[0]; assign sd_ck_p = ddr_clk[0]; assign sd_cke = ddr_cke; assign sd_cs = ddr_csb; assign sd_a = ddr_address; assign sd_ras = ddr_rasb; assign sd_cas = ddr_casb; assign sd_we = ddr_web; assign sd_udm = ddr_dm[1]; assign sd_ldm = ddr_dm[0]; assign sd_ba = ddr_ba; endmodule
テストベンチTOPです。
テストベンチのダウンロード
ハードウェアトップのダウンロード
// DDRtest_tb.v //`default_nettype none //Mar.4.2008 Tak.Sugawara marseeさんのテストベンチTOPをRegressionTestがしやすいように書き換えた。 //Mar.4.2008 Test方法追加修正 //Mar.5.2008 Random W/R with burst access 追加 //Mar.6.2008 lfsr22バグ修正 burst_wordsをランダムにするテスト追加 //Mar.6.2008 sim_clockをclk_outに統一 Timeout Check追加 //Mar.7.2008 random routine w/random butst lengthをタスク化 //Mar.9.2008 FIFO待ちバグ修正 //TODO burst read/write test with random pending time `timescale 1ps / 1ps module DDRtest_tb; `include "ddr_parameters.vh" wire [DQ_BITS-1:0] dq; wire [DQS_BITS-1:0] dqs; reg async_reset; wire [DQS_BITS-1:0] ddr_dqs_fpga, ddr_dqs_sdram; wire [DQ_BITS-1:0] ddr_dq_fpga, ddr_dq_sdram; reg [DQS_BITS-1:0] ddr_dqs_fpgan, ddr_dqs_sdramn; reg [DQ_BITS-1:0] ddr_dq_fpgan, ddr_dq_sdramn; wire ddr_clk, ddr_clkb; wire ddr_cke, ddr_csb, ddr_rasb, ddr_casb, ddr_web; wire [DM_BITS-1:0] ddr_dm; wire [1:0] ddr_ba; wire [ADDR_BITS-1:0] ddr_address; wire reset_b; reg enable_o; wire [2:0] cmd; reg sdram_clk; reg sdram_clkb; reg [12:0] sdram_address; reg [1:0] sdram_ba; reg sdram_cke; reg sdram_csb, sdram_rasb, sdram_casb, sdram_web; reg [1:0] sdram_dm; wire ddr_clk_fb; parameter DELAY_TIME = 1500; parameter CLK_PERIOD = 20000; assign cmd = {ddr_rasb, ddr_casb, ddr_web}; always @(posedge ddr_clk, posedge async_reset) if (async_reset) enable_o <= 1'b0; else if (cmd==3'b100) enable_o <= 1'b0; else if (cmd==3'b101) enable_o <= 1'b1; always @ * if (enable_o == 1'b1) ddr_dqs_fpgan <= #DELAY_TIME ddr_dqs_sdram; else ddr_dqs_fpgan <= #DELAY_TIME {DQS_BITS{1'bz}}; always @ * if (enable_o == 1'b1) ddr_dq_fpgan <= #DELAY_TIME ddr_dq_sdram; else ddr_dq_fpgan <= #DELAY_TIME {DQ_BITS{1'bz}}; always @ * if (enable_o == 1'b0) ddr_dqs_sdramn <= #DELAY_TIME ddr_dqs_fpga; else ddr_dqs_sdramn <= #DELAY_TIME {DQS_BITS{1'bz}}; always @ * if (enable_o == 1'b0) ddr_dq_sdramn <= #DELAY_TIME ddr_dq_fpga; else ddr_dq_sdramn <= #DELAY_TIME {DQ_BITS{1'bz}}; assign ddr_dqs_fpga = ddr_dqs_fpgan; assign ddr_dq_fpga = ddr_dq_fpgan; assign ddr_dqs_sdram = ddr_dqs_sdramn; assign ddr_dq_sdram = ddr_dq_sdramn; always @ * begin sdram_clk <= #DELAY_TIME ddr_clk; sdram_clkb <= #DELAY_TIME ddr_clkb; sdram_address <= #DELAY_TIME ddr_address; sdram_ba <= #DELAY_TIME ddr_ba; sdram_cke <= #DELAY_TIME ddr_cke; sdram_csb <= #DELAY_TIME ddr_csb; sdram_rasb <= #DELAY_TIME ddr_rasb; sdram_casb <= #DELAY_TIME ddr_casb; sdram_web <= #DELAY_TIME ddr_web; sdram_dm <= #DELAY_TIME ddr_dm; end //Host IF reg clk_fm_host=0; reg [31:0] address_fm_host=0,input_data_fm_host=0; reg read_fm_host=0,write_fm_host=0; reg [3:0] byte_enable_fm_host=4'hf;//書き込みバイトEnable wire [31:0] output_data_fm_controller; wire fifo_full_fm_controller;//address full または Write Fifo Full wire read_valid_fm_controller; wire initialize_end_fm_controller; wire clk_out; ddr_controller hardware_top( //非同期リセット .async_reset(async_reset), //Host->コントローラ .clk_fm_host(clk_fm_host), .input_address_fm_host(address_fm_host), .input_data_fm_host(input_data_fm_host), .read_fm_host(read_fm_host), .write_fm_host(write_fm_host), .byte_enable_fm_host(byte_enable_fm_host),//書き込みバイトEnable //コントローラ-> Host .clk_out(clk_out), .output_data_fm_controller(output_data_fm_controller), .fifo_full_fm_controller(fifo_full_fm_controller),//address full または Write Fifo Full .read_valid_fm_controller(read_valid_fm_controller), .initialize_end_fm_controller(initialize_end_fm_controller), .sd_a(ddr_address), .sd_dq(ddr_dq_fpga), .sd_ba(ddr_ba), .sd_ras(ddr_rasb), .sd_cas(ddr_casb), .sd_we(ddr_web), .sd_udm(ddr_dm[1]), .sd_ldm(ddr_dm[0]), .sd_udqs(ddr_dqs_fpga[1]), .sd_ldqs(ddr_dqs_fpga[0]), .sd_cs(ddr_csb), .sd_cke(ddr_cke), .sd_ck_n(ddr_clkb), .sd_ck_p(ddr_clk), .sd_ck_fb(ddr_clk_fb) ); assign ddr_clk_fb = ddr_clk; ddr MT46V16M16_inst( .Dq(ddr_dq_sdram), .Dqs(ddr_dqs_sdram), .Addr(sdram_address), .Ba(sdram_ba), .Clk(sdram_clk), .Clk_n(sdram_clkb), .Cke(sdram_cke), .Cs_n(sdram_csb), .Ras_n(sdram_rasb), .Cas_n(sdram_casb), .We_n(sdram_web), .Dm(sdram_dm) ); initial begin async_reset = 1'b0; #1000 async_reset = 1'b1; #20000 async_reset = 1'b0; end always begin #(CLK_PERIOD/2) clk_fm_host = 1'b1 ; #(CLK_PERIOD/2) clk_fm_host = 1'b0 ; end reg [31:0] read_fifo [0: 2**23-1];//24bit fifo //テストメインループ initial begin $timeformat(-9,1,"nsec",8); //準備ができるまで待つ initial_wait; //シングルライト/リード check_random_single_write_read(10); //インクリメンタルチェック check_incremental_single_write_read(10); //バースト ライト/リード 固定アドレス burst_check_fixed_pattern(0,10,32'h5555_5555);// burst_check_fixed_pattern(0,258,32'hAAAA_AAAA);// //バースト ライト/リード ランダムアドレス random_write_read_w_random_bust_length(100000,19);//ループ数 ,最大パースト長+1をセット Mar.7.2009 20以上は仕様外?? #1000000 $finish; end task random_write_read_w_random_bust_length(input integer counts,max_words); reg [23:0] seed_address,write_temp_address,read_temp_address; integer seed_data,write_temp_data,read_temp_data; integer burst_words,burst_words_seed; integer test_counter; begin seed_address=1<<2; seed_data=1; write_temp_address=seed_address; write_temp_data=seed_data; read_temp_address=seed_address; read_temp_data=seed_data; burst_words_seed=1; burst_words=burst_words_seed; test_counter=0; repeat(counts) begin $display("Start burst write random burst_words=%d %t",burst_words,$time); burst_write_random(write_temp_address,write_temp_data,burst_words); //$display("Start burst read random %t",$time); burst_read_random(read_temp_address,burst_words); burst_read_check_random(read_temp_data ,burst_words); test_counter=test_counter+1; if (test_counter%100==0) $display("Pass %d",test_counter); burst_words_seed=lfsr8(burst_words_seed); if(burst_words_seed> max_words) begin burst_words=burst_words_seed% max_words; if (!burst_words) burst_words=1; end end end endtask task initial_wait; begin #10; wait(initialize_end_fm_controller); end endtask task check_random_single_write_read(input integer counts); integer address,write_data,read_data; begin $display("Start check_random_single_write_read %t",$time); repeat(counts) begin address=$random; write_data=$random; single_write(address,write_data); single_read(address,read_data); if (write_data !==read_data) begin $display("In check_random_single_write_read. Error Detected address=%h wd=%h rd=%h",address,write_data,read_data); $stop; end end end endtask task check_incremental_single_write_read(input integer counts); integer address,write_data,read_data; begin $display("Start check_inc_single_write_read %t",$time); address=0; write_data=0; repeat(counts) begin single_write(address,write_data); single_read(address,read_data); if (write_data !==read_data) begin $display("In check_inc_single_write_read. Error Detected address=%h wd=%h rd=%h",address,write_data,read_data); $stop; end address=address+4; write_data=write_data+1; end end endtask task single_write(input integer address,data); begin wait(!fifo_full_fm_controller); @(negedge clk_out); write_fm_host=1; input_data_fm_host=data; address_fm_host=address; @(negedge clk_out); write_fm_host=0; input_data_fm_host=data; end endtask task single_read(input integer address, output [31:0] data); begin wait(!fifo_full_fm_controller );//コントローラがBusyなら待つ @(negedge clk_out);//Here we go! リードコマンド書き込み//Mar.6.2008 clk_outに変更 read_fm_host=1; address_fm_host=address; @(negedge clk_out);//Mar.6.2008 clk_outに変更 read_fm_host=0; wait(read_valid_fm_controller);//データが来るのを待つ @(negedge clk_out);//着た data=output_data_fm_controller;//読み込み end endtask task burst_check_fixed_pattern(input integer address,counts,data); begin burst_write_fixed(address,counts,data); burst_read(address,counts); burst_read_check_fixed(address,counts); end endtask function [21:0] lfsr22 (input [21:0] data); reg lsb; //M系列タップ値は、以下から頂きました。 //http://www.madlabo.com/mad/edat/mathematic/M/index.htm#SEC5 //200001 //0010_0000_0000_0000_0000_0011 begin lsb=data[21] ^ data[1]^data[0]; lfsr22={data[20:0],lsb};//MSB 方向にシフト end endfunction function [7:0] lfsr8 (input [7:0] data); reg lsb; //CD polynominal //1001_1101 begin lsb=data[7] ^ data[4]^data[3] ^data[2]^data[0]; lfsr8={data[6:0],lsb};//MSB 方向にシフト end endfunction //24ビットのダブらないランダムアドレスを得る //4bytes boundary 0は、不可 function [23:0] get_next_random_address ( input [23:0] old_address); //assert( !old_address); get_next_random_address=lfsr22(old_address >>2) <<2; endfunction task burst_write_random(inout integer seed_address,seed_data ,input integer counts); integer counter; begin //assert(read_fm_host==0); //ライトコマンドをcounts分送出 counter=0; repeat(counts) begin @(negedge clk_out); if (fifo_full_fm_controller) begin write_fm_host=0; wait(!fifo_full_fm_controller); @(negedge clk_out); end write_fm_host=1; input_data_fm_host=seed_data; address_fm_host =seed_address; seed_address=get_next_random_address(seed_address); seed_data=$random(seed_data); counter=counter+1; end @(negedge clk_out); write_fm_host=0; //ライトコマンド送出終わり end endtask task burst_write_fixed(input integer address,counts,data); integer counter; begin //assert(read_fm_host==0); //ライトコマンドをcounts分送出 counter=0; repeat(counts) begin @(negedge clk_out);//Mar.6.2008 clk_outに変更 if (fifo_full_fm_controller) begin write_fm_host=0; wait(!fifo_full_fm_controller); @(negedge clk_out); end write_fm_host=1; input_data_fm_host=data; address_fm_host=address+(counter<<2); counter=counter+1; end @(negedge clk_out); write_fm_host=0; //ライトコマンド送出終わり end endtask `define TIMEOUT_CLKS 50 //*10ns* burst_words task time_out_check_fifo(input integer counts); begin :time_out_exit repeat(`TIMEOUT_CLKS*counts ) begin if (fifo_full_fm_controller) begin//Mar.9.2008 @(negedge clk_out); end else begin @(negedge clk_out); disable time_out_exit;//repeat ループを抜ける end end //`TIME_COUT_COUNT CLK 経ってもFIFO BUSYならエラ-だ。 $display("Time Out Error Please Check H/W Logic %t",$time); $stop; end endtask task time_out_check_read_valid(input integer counts); begin :time_out_exit repeat(`TIMEOUT_CLKS *counts) begin if (!read_valid_fm_controller) begin @(negedge clk_out); end else begin //@(negedge clk_out); disable time_out_exit;//repeat ループを抜ける end end //`TIME_COUT_COUNT CLK 経ってもFIFO BUSYならエラ-だ。 $display("Time Out Error Please Check H/W Logic %t",$time); $stop; end endtask task burst_read(input integer address,counts); integer send_counter,read_counter; begin send_counter=0;//Mar.6.2008 //assert(read_fm_host==0); //リードコマンドをcounts分送出 //assert(!read_valid_fm_controller); fork//コマンドを送っている間にもReadValidは来るので送信と受信は、平行プロセスでなければならない。 begin repeat(counts) begin @(negedge clk_out);//Mar.6.2008 clk_outに変更 if (fifo_full_fm_controller) begin read_fm_host=0; time_out_check_fifo(counts);//Mar.6.2008 タイムアウトチェック追加 //wait(!fifo_full_fm_controller); //@(negedge clk_out); end read_fm_host=1; address_fm_host=address+(send_counter<<2); send_counter=send_counter+1; end @(negedge clk_out); read_fm_host=0; //リードコマンド送出終わり end //リードデータをcounts分読み込む begin read_counter=0; wait(send_counter !==0);//Mar.6.2008 Timeout Check のため repeat(counts) begin @(negedge clk_out);//Read 読み込みは、100MHzで来るので100MHzで受ける if (!read_valid_fm_controller) begin time_out_check_read_valid(counts);//Mar.6.2008タイムアウトチェック追加 //wait(read_valid_fm_controller); //@(negedge clk_out);//Read 読み込みは、100MHzで来るので100MHzで受ける end read_fifo[read_counter]=output_data_fm_controller; read_counter=read_counter+1; end end join end endtask task burst_read_check_fixed(input integer counts,data); integer counter; begin counter=0; repeat(counts) begin if (read_fifo[counter] !==data) begin $display("Error Detected burst_read_check_fixed %t",$time); $stop; end counter=counter+1; end end endtask task burst_read_check_random(inout integer seed_data,input integer counts); integer counter; begin counter=0; repeat(counts) begin if (read_fifo[counter] !==seed_data) begin $display("Error Detected burst_read_check_random %t %h %h ",$time,read_fifo[counter],seed_data); $stop; end counter=counter+1; seed_data=$random(seed_data); end end endtask task burst_read_random(inout integer seed_address,input integer counts); integer send_counter,read_counter; begin send_counter=0; //assert(read_fm_host==0); //リードコマンドをcounts分送出 //assert(!read_valid_fm_controller); fork//コマンドを送っている間にもReadValidは来るので送信と受信は、平行プロセスでなければならない。 begin repeat(counts) begin @(negedge clk_out);//Mar.6.2008 clk_outに変更 if (fifo_full_fm_controller) begin read_fm_host=0; wait(!fifo_full_fm_controller); @(negedge clk_out); end read_fm_host=1; address_fm_host=seed_address; seed_address=get_next_random_address(seed_address); send_counter=send_counter+1; end @(negedge clk_out); read_fm_host=0; //リードコマンド送出終わり end //リードデータをcounts分読み込む begin read_counter=0; wait(send_counter !==0);//Mar.6.2008 Timeout Check のため repeat(counts) begin @(negedge clk_out);//Read 読み込みは、100MHzで来るので100MHzで受ける if (!read_valid_fm_controller) begin time_out_check_read_valid(counts);//Mar.6.2008 タイムアウトチェック追加 //wait(read_valid_fm_controller); //@(negedge clk_out);//Read 読み込みは、100MHzで来るので100MHzで受ける end read_fifo[read_counter]=output_data_fm_controller; read_counter=read_counter+1; end end join end endtask endmodule
テストベンチ解説
少し有意義なベンチかもしれませんので解説します。
(1) taskにしたい。しかし平行プロセスの場合どう記述する?
アドレスデータ送出中であっても、rddata_validは100MHzで連続的に来ます。下のタイムチャートのように、addr_fifo_wrenでアドレス送出中であっても rddata_validがアサートしているのがわかります。rdata_validが着たときにすぐにデータをラッチしないとデータを失ってしまいます。(筆者も最初これをシーケンシャルで書いていてコンペアエラーになってしまいました。)
つまり、送信と受信は平行プロセスでなければなりません。こういうときは、上のように fork joinで記述で記述します。この場合 アドレス要求数と、ACKとしてのrddata_valid 数は、常に一致している筈です。どちらのプロセスも終了したときに、(つまり、アドレス送信の結果、全部rddata_validを受け取って)一つのお仕事(task)が完了(join)したと考えます。fork joinについては、EDA EXPRESSさんのこの資料が分かりやすいです。
(2)wait って何?
ところで、wait という構文は、解説したことがありませんでした。@(xx )は、エッジセンスでブロッキング(つまり、その構文に出会うとスルーしないで、変化を待ちます。いわばエッジセンシティブです。ところが、waitは、レベルセンシティブで、その信号状態がTrueだとブロッキングしません。(素通りします)
(3)ダブりのないランダムアドレスを作りたい
M系列(LFSR)で作ります。ダブりのないアドレス生成には最適です。ただし、0は永久に0から抜け出ることがないのでそれ以外の一巡になります。ソース中のサイトを参照すれば、30ビットまでのタップ位置は分かります。
(4)$randomを制御したい
任意のランダムアドレスにランダムを書いて、連想配列で覚えておくのも手ですが、ここでは、Seed=$random(Seed)を使って、ライトとリード時で同じシーケーンスになるようにしてしています。それをコンペアすることで配列を使わずにすみます。DDR等、巨大なメモリを長時間テストをするときに使える手かもしれません。task内では、ローカル変数になりますからtaskから抜けるときに現在のSeed値を覚えておく必要がありますね。そういう場合は、task引数をinout
にして、taskから抜けるときに書き戻してやります。このようにしてSeedシーケンスを継続させることができます。しかし、$randomは、デザイン全体に渡るグローバルStatic変数でインスタンスオブジェクトの概念が適用できません。なので、そのほかに、ランダム値を使いたいときは、所望のビット幅のLFSRを作成してランダム値を得るようにしています。(SVクラスならエレガントに書けそうですが。)
上のベンチでは、ddr_parameter.vh を次のように設定しています。 "FULL_MEMでない!"とDDRが文句を言ってきたので入れました。(親切なモデリングIPですね。) また、DEBUGを0にするのは、コンソールに大量にメッセージ(Verbose)されるのを防ぐためです。このようにしてもエラーのときは、ちゃんと出てきます。
parameter DEBUG = 0; //TAK Turn on DEBUG message `define FULL_MEM //TAK
実機検証環境の構築
以下は、実機を用いた検証イメージです。
FPGA内蔵のSRAMに小さいCプログラムを置き、Wishboneバスを介してYACCが動きます。検証コアは、 キャッシュを介さないIOポート領域に適当にMAPします。
ホストPCとUARTを介しコマンドやパラメータの送信、結果(エラーレート)の表示を行います。問題は、検証コア以前にそれ以外のCPUを含むブロックは、一切実機で動かしたことがないことです。まず、検証コア以外のブロックは、設計と検証が必要ですね。果たして動くでしょうか?
ところで、現状は、FIFO付きUARTポートとSRAMは、接続していません 。まずそれらをつなぐことが必要です。UartReadは、CPUの割り込み端子につなぎ割り込み処理することも必要です。とりあえずは、これをつないだRTLを作成しシミュレーションによる検証を行うことにします。
SRAMの接続
Xilinx Spartan3Eキットなので、XilinxのCoreGenでRAMを生成させます。その際初期化ファイルとしてCコンパイルUtilityが生成したCoeファイルを指定します。これでRAMを生成させると次のような記述で生成されます。SRAMは、バイト幅で8K生成させます。1WORD=4BYTEなので合計4個で32KBのRAM生成になります。
さて、この後、Cコンパイル->シミュレーションを何回かすることになりますが、そのたびにいちいちCoreGenでRAM初期値を指定して生成させるのは面倒ですし時間がかかりますのでCコンパイルUtilityでは、このRAMが呼び出している実質のmifファイルも一緒に生成するようにしています。(AlteraのMIFとは違います。unisimライブラリは、これを$readmembで読むだけです。)つまり雛形だけCoreGenで生成させて中身はすり替えてシミュレーションします。ただし、論理合成はあくまで、CoreGenで生成した時点のNGCファイルを使いますの注意してください。
module ram8k0( addr, clk, din, dout, we); input [12 : 0] addr; input clk; input [7 : 0] din; output [7 : 0] dout; input we; // synthesis translate_off BLKMEMSP_V6_2 #( .c_addr_width(13), .c_default_data("0"), .c_depth(8192), .c_enable_rlocs(0), .c_has_default_data(0), .c_has_din(1), .c_has_en(0), .c_has_limit_data_pitch(0), .c_has_nd(0), .c_has_rdy(0), .c_has_rfd(0), .c_has_sinit(0), .c_has_we(1), .c_limit_data_pitch(18), .c_mem_init_file("ram8k0.mif"),..
UARTの接続
最近は、シリアルポートのないPCが増えて残念なのですが、組み込みの現場ではまだまだ現役ではないでしょうか?
旧YACCプロジェクトで作ったUARTをそのまま使います。このUARTは、WRITE FIFO512バイト付きです。ファームウェアは、1バイトづつポートに書き込みますが、その際FIFOがFULLかどうか常にチェックしながら書き込む必要があります。READについてはFIFOはなく1バイト毎に割り込みが発生する仕様なので、それを割り込み処理する必要があります。
また、UARTのポートマッピングは、専用のNONキャッシュ領域にマップします。(IOはキャッシュ領域にはマップできません。..)
WISHBONEバスに独自のIOを追加する場合は、このUARTのポート記述を参考にしてください。
SRAMとUARTを含むハードウェアTOPは、次の通りです。Wishboneバスコントローラに2つのマスタと2つのスレーブがつながります。マスタは、D-CacheとI-Cache、スレーブは、SRAM(32KB)とIO領域(非キャッシュ領域になります。) IO領域は、FIFO付きUART WRITEブロック、とREADブロックが接続されます。未だ、marseeさんのDDRコントローラを接続する段階ではありません。このハードウェアブロック単体での動作確認を実機で行った後に、SRAMコントローラ検証ポートを非キャッシュ領域にマップします。
module hardware_top( input clk,rst, input RXD, output TXD ); wire [31:0] m0_data_i; wire [31:0] m0_data_o; wire [31:0] m0_addr_i; wire [3:0] m0_sel_i; wire m0_we_i; wire m0_cyc_i; wire m0_stb_i; wire m0_ack_o; wire m0_err_o; wire m0_rty_o; wire [31:0] m1_data_i; wire [31:0] m1_data_o; wire [31:0] m1_addr_i; wire [3:0] m1_sel_i; wire m1_we_i; wire m1_cyc_i; wire m1_stb_i; wire m1_ack_o; wire m1_err_o; wire m1_rty_o; //Slave ports wire [31:0] s0_data_i,s1_data_i; wire [31:0] s0_addr_o,s1_addr_o; wire [31:0] s0_data_o,s1_data_o; wire [3:0] s0_sel_o,s1_sel_o; wire s0_we_o,s1_we_o; wire s0_cyc_o,s1_cyc_o; wire s0_stb_o,s1_stb_o; wire s0_err_i,s1_err_i; wire s0_rty_i,s1_rty_i; wire [5:0] interrupt; wire s0_ack_i,s1_ack_i; wire uart_read_int; assign interrupt={4'b0000,uart_read_int};//uart intをCPUのinterrupt portに接続 ///////////////////////////////////////////////////////////////////// // // WISHBONE Inter Connect // //バスコントローラ wb_conbus_top #(4, 4'h0, 4, 4'h1, 4, 4'h2, 4'h3, 4'h4, 4'h5, 4'h6, 4'h7 ) conbus( .clk_i( clk ), .rst_i( rst ), .m0_dat_i( m0_data_i ), .m0_dat_o( m0_data_o ), .m0_adr_i( m0_addr_i ), .m0_sel_i( m0_sel_i ), .m0_we_i( m0_we_i ), .m0_cyc_i( m0_cyc_i ), .m0_stb_i( m0_stb_i ), .m0_ack_o( m0_ack_o ), .m0_err_o( m0_err_o ), .m0_rty_o( m0_rty_o ), .m1_dat_i( m1_data_i ), .m1_dat_o( m1_data_o ), .m1_adr_i( m1_addr_i ), .m1_sel_i( m1_sel_i ), .m1_we_i( m1_we_i ), .m1_cyc_i( m1_cyc_i ), .m1_stb_i( m1_stb_i ), .m1_ack_o( m1_ack_o ), .m1_err_o( m1_err_o ), .m1_rty_o( m1_rty_o ), //仮想RAM .s0_dat_i( s0_data_i ), .s0_dat_o( s0_data_o ), .s0_adr_o( s0_addr_o ), .s0_sel_o( s0_sel_o ), .s0_we_o( s0_we_o ), .s0_cyc_o( s0_cyc_o ), .s0_stb_o( s0_stb_o ), .s0_ack_i( s0_ack_i ), .s0_err_i( s0_err_i ), .s0_rty_i( s0_rty_i ), //仮想ポート .s1_dat_i( s1_data_i ), .s1_dat_o( s1_data_o ), .s1_adr_o( s1_addr_o ), .s1_sel_o( s1_sel_o ), .s1_we_o( s1_we_o ), .s1_cyc_o( s1_cyc_o ), .s1_stb_o( s1_stb_o ), .s1_ack_i( s1_ack_i ), .s1_err_i( s1_err_i ), .s1_rty_i( s1_rty_i ) ); ///////////////////////////////////////////////////////////////////// // // WISHBONE Slave Models // //仮想SRAMモジュールを接続 onchip_ram_top onchip_ram( .wb_clk_i( clk ), .wb_rst_i( rst ), .wb_adr_i( s0_addr_o ), .wb_dat_i( s0_data_o ), .wb_dat_o( s0_data_i ), .wb_cyc_i( s0_cyc_o ), .wb_stb_i( s0_stb_o ), .wb_sel_i( s0_sel_o ), .wb_we_i( s0_we_o ), .wb_ack_o( s0_ack_i ), .wb_err_o( s0_err_i ), .wb_rty_o( s0_rty_i ) ); //仮想IOモジュールを接続 virtual_io_port virtual_io_port( .wb_clk_i( clk ), .wb_rst_i( rst ), .wb_adr_i( s1_addr_o ), .wb_dat_i( s1_data_o ), .wb_dat_o( s1_data_i ), .wb_cyc_i( s1_cyc_o ), .wb_stb_i( s1_stb_o ), .wb_sel_i( s1_sel_o ), .wb_we_i( s1_we_o ), .wb_ack_o( s1_ack_i ), .wb_err_o( s1_err_i ), .wb_rty_o( s1_rty_i ), .TXD(TXD), .RXD(RXD), .uart_read_int(uart_read_int) ); ///////////////////////////////////////////////////////////////////// // // WISHBONE Master Models // //CPU部(Iキャッシュ、Dキャッシュ含む) wishbone_yacc yacc_as_wb_master( //common port .wb_clk_i( clk ), .wb_rst_i( rst ), //master port 0 //YACC インストラクションキャッシュ .wb_m0_addr_o( m0_addr_i ), .wb_m0_dat_i( m0_data_o ), .wb_m0_dat_o( m0_data_i ), .wb_m0_cyc_o( m0_cyc_i ), .wb_m0_stb_o( m0_stb_i ), .wb_m0_sel_o( m0_sel_i ), .wb_m0_we_o( m0_we_i ), .wb_m0_ack_i( m0_ack_o ), .wb_m0_err_i( m0_err_o ), .wb_m0_rty_i( m0_rty_o ), //master port1 //YACC データキャッシュ .wb_m1_addr_o( m1_addr_i ), .wb_m1_dat_i( m1_data_o ), .wb_m1_dat_o( m1_data_i ), .wb_m1_cyc_o( m1_cyc_i ), .wb_m1_stb_o( m1_stb_i ), .wb_m1_sel_o( m1_sel_i ), .wb_m1_we_o( m1_we_i ), .wb_m1_ack_i( m1_ack_o ), .wb_m1_err_i( m1_err_o ), .wb_m1_rty_i( m1_rty_o ), .interrupt(interrupt) ); endmodule
UARTを含むCPUブロックの検証
それでは、UARTとSRAMを含む検証はどうすればよいでしょうか?簡単なCプログラムを書いてみましょう。いままでコンソール出力は、仮想ポートを使用してきましたが、今度は、本物の(HDLで書いたハードウェア)ポート(UART)を駆動します。
インタラクティブなUARTのシミュレーションの仕方
Cで電卓のプログラムを書いています。さて UARTでインタラクティブに
をシミュレーションするにはどうしたらよいでしょうか?
それには、HOST PCのUARTポートのテストベンチで書いて、合成対象であるUARTを含むCPUと通信させればよいですね。下が、HOSTPCのUARTをシミュレーションするテストベンチです。
UARTは、CPU内で持っているUARTと同じモジュールで違うインスタンスになります。検証済みのH/Wは、テストベンチで部品として利用できます。
合成対象である、CPUは、上のブロック図でDDRコントローラ以外の全てのH/Wを内包しています。しかし、クロックとリセット、それにUARTポートのRXD,TXDしかありません。
CPUから送られてくるRXDは、ビットの羅列でしかないので、人間が見やすいようにHOST PCのUARTでコンソールに出力してやります。
また、人間がインタラクティブに入力するキーボード入力は、タスク化し、
send_uart_str("1+1\n");
という感じに記述します。人間が入力するのは、YACC>というメッセージを見てから入力するので、タスク内では、
wait(buffer_reg==">");//プロンプトを待つ
の後に入力する(UARTでCPUに送る)という具合です。
`timescale 1ns/1ps `include "define.h" module tb_wb_conbus; reg clk=0; reg rst=1; wire TXD, RXD; initial begin rst=1; #101; rst = 0; // HERE IS WHERE THE TEST CASES GO ... //$finish; end // End of Initial ///////////////////////////////////////////////////////////////////// // // Clock Generation // `ifdef DELAY_SIM always #(18) clk = ~clk; // initial begin // $dumpvars(1,tb_wb_conbus.dut); // #20000; // $dumpflush; // $finish; // end `else always #10 clk = ~clk; `endif `ifdef ECHO_BACK assign RXD=TXD;//エコーバック `endif //合成対象 hardware_top dut( .clk,.rst,.RXD,.TXD ); `define SIMULATE_HOST_UART `ifdef SIMULATE_HOST_UART //仮想UART YACCから送られてくるTXDを解釈し コンソールに出力する //uart read port wire [7:0] buffer_reg; wire int_req; reg sync_reset; always @(posedge clk, posedge rst) begin if (rst) sync_reset <=1'b1; else sync_reset<=1'b0; end uart_read uart_read_port( .sync_reset(sync_reset), .clk(clk), .rxd(TXD),.buffer_reg(buffer_reg), .int_req(int_req)); `ifndef ECHO_BACK //ホストの送り側UARTブロック reg [7:0] host_write_byte; reg uart_write=0; wire uart_busy; uart_write uwrite( .sync_reset(sync_reset), .clk(clk), .txd(RXD ), .data_in(host_write_byte) , .write_request(uart_write ), .write_done(), .write_busy(uart_busy) ); task sleep(input integer count); repeat(count) begin @(posedge clk); end endtask initial begin wait(!sync_reset); send_uart_str("1+1\n"); send_uart_str("1000+1001\n"); send_uart_str("(-105)/(-31)\n"); send_uart_str("(-105)/(31)\n"); send_uart_str("(1050)/(-31)\n"); send_uart_str("105%31\n"); send_uart_str("-105%31\n"); send_uart_str("(-105)%(-31)\n"); send_uart_str("(-105)%(31)\n"); send_uart_str("(1050)%(-31)\n"); send_uart_str("2147483647/13213210\n"); send_uart_str("(-2147483647)/1654760\n"); send_uart_str("(-2147483647)/(-14320)\n"); send_uart_str("(+2147483647)/(-153420)\n"); send_uart_str("2147483647%14324320\n"); send_uart_str("(-2147483647)%14320\n"); send_uart_str("(-2147483647)%(-1540)\n"); send_uart_str("(+2147483647)%(-143220)\n"); send_uart_str("-17/(1)\n"); send_uart_str("117/(-1)\n"); send_uart_str("-(-13432234)/(-4323437)\n"); send_uart_str("-(-143243243)/(743243)\n"); send_uart_str("-123459*(-3454)\n"); send_uart_str("-1321*(73213)\n"); send_uart_str("+543541*(7213)\n"); send_uart_str("+1432*(-7322)\n"); send_uart_str("-165*(-7232)\n"); send_uart_str("-(1321)*(-7111)\n"); send_uart_str("-(-1543)*(-743243)\n"); sleep(50000);//結果表示待ち $finish; end task send_uart_byte(input [7:0] byte); begin wait(!uart_busy); @(negedge clk); host_write_byte=byte; uart_write=1; @(negedge clk); uart_write=0; end endtask task send_uart_str (input [8*100-1:0] str); integer counter; begin wait(buffer_reg==">");//プロンプトを待つ counter=0; while (str[ 8*counter +:8] !==0) counter=counter+1;//文字数を数える if (counter) counter=counter-1;//1以上あるなら-1にして最後の文字を指させる while(counter>=0) begin//後ろから送る send_uart_byte(str[8*counter +:8]); counter=counter-1; end wait(buffer_reg !=">");//レスポンスを待つ end endtask `endif integer i=0; always @(posedge int_req) begin begin :local localparam LF=8'h0a; reg [7:0] local_mem [0:1000]; if (i>=1000) begin $display("Too many Characters."); $stop;//assert(0); end if (buffer_reg==LF) begin :local2 //pop stack integer j; j=0; while( j < i) begin $write( "%c",local_mem[j]); j=j+1; end if (local_mem[1]=="$" && local_mem[2]=="f" && local_mem[3]=="i" && local_mem[4]=="n" && local_mem[5]=="i" && local_mem[6]=="s") $finish; else if (local_mem[1]=="$" && local_mem[2]=="t" && local_mem[3]=="i" && local_mem[4]=="m" && local_mem[5]=="e") $display(" time=%t",$time); // $write(" : time=%t\n",$time); i=0;//clear stack end else begin//push stack local_mem[i]=buffer_reg; i=i+1; end end end `endif endmodule
これをシミュレーションした様子です。
YACC> が CPUが出力したプロンプト で 1+1が人間の入力を模擬してHOSTPC UARTから送ったものですが、表示の1+1はCPUがエコーバックした結果です。
このように、テストベンチを書くことによって、HDLシミュレータ上でかなりの部分をシミュレート可能です。(気合のある方は、VPIを使って実際にGUIでキーボートから入力しても面白いかもしれません。)Cコンパイルから開始から、HDLのコンパイル完了まで3秒位です。シミュレーション全体のRUN時間は、Celeron1.2GHzで7分位でしたが、最初のメッセージまでは、数秒で、快適な開発環境だと思います。シミュレーションが速いのは、CPUのソースがRTLであることに起因します。FPGAベンダのCPUもベンダプリミティブではなく、RTLで出してくれれば、速くなるのですが..)
階層スコープを下に出していますが、dut以下が合成対象になります。
tb_wb_conbus | テストベンチTOP |
dut | 合成階層TOP |
conbus | Wishboneバスコントローラ |
onchip_ram | 8Kwordsx4 =32KSRAM |
virtual IO port | UART |
yacc_as_wb_master | キャッシュ付きYACC |
ところで、RDXは、1バイトづつの割り込みになります。従って割り込み処理プログラムも書かなくてはいけません。
ポート定義は、がyacc_port.hがCヘッダ, define.h がHDL上のポート定義になります。
//YACC Definitions
#define UART_PORT_ADDRESS 0x1ffffc //
ポートのビット定義は以下です。W/Rアドレスは共通でBit[8]がFIFO FULLであることを示しています。
アドレス | R | W |
UART_PORT_ADDRESS | Read Data:[7:0] WriteBusy [8] 1でFIFO FULL、0でWrite Ready [31:9] :Reserved |
WriteData:[7:0] |
CでのUART R/W ルーチンです。
void print_uart(unsigned char* ptr)// { unsigned int uport; #define WRITE_BUSY 0x0100 while (*ptr) { do { uport=*(volatile unsigned*) uart_port; } while (uport & WRITE_BUSY); *(volatile unsigned char*)uart_wport=*(ptr++); } //*(volatile unsigned char*)uart_wport=0x00;//Write Done } void putc_uart(unsigned char c)// { unsigned int uport; do { uport=*(volatile unsigned*) uart_port; } while (uport & WRITE_BUSY); *(volatile unsigned char*)uart_wport=c; } char read_uart()//Verilog Test Bench Use { unsigned uport; uport= *(volatile unsigned *)uart_rport; return uport; }
割り込みハンドラは、以下のアセンブラのソースになりますが、ユーザは、なんらメンテする必要がありません。次のスタートアップファイルとリンクするだけです。(乗除算セーブを追加しました。)
################################################################## # TITLE: Boot Up Code # AUTHOR: Steve Rhoads (rhoadss@yahoo.com) # DATE CREATED: 1/12/02 # FILENAME: boot.asm # PROJECT: Plasma CPU core # COPYRIGHT: Software placed into the public domain by the author. # Software 'as is' without warranty. Author liable for nothing. # DESCRIPTION: # Initializes the stack pointer and jumps to main2(). ################################################################## .text .align 2 .globl entry .ent entry entry: .set noreorder #These eight instructions must be the first instructions. #convert.exe will correctly initialize $gp lui $gp,0 ori $gp,$gp,0 #convert.exe will set $4=.sbss_start $5=.bss_end lui $4,0 ori $4,$4,0 lui $5,0 ori $5,$5,0 lui $sp,0 ori $sp,$sp,0xfff0 #initialize stack pointer $BSS_CLEAR: sw $0,0($4) slt $3,$4,$5 bnez $3,$BSS_CLEAR addiu $4,$4,4 sw $5,bss_end_save lui $4,0 ori $4,0xff01 #現状態 カーネルモード、割り込みEnable/割り込みEnableビットSET mtc0 $4,$12 #割り込みEnable jal main2 # C メインに飛ぶ nop $L1: j $L1 .align 4 .globl bss_end_save bss_end_save: .long 0 .org 0x80 #.set noreorder #address 0x3c interrupt_service_routine:#インタラプトハンドラ addi $sp, $sp, -25*4 sw $1, 1*4($sp) sw $2, 2*4($sp) sw $3, 3*4($sp) sw $4, 4*4($sp) sw $5, 5*4($sp) sw $6, 6*4($sp) sw $7, 7*4($sp) sw $8, 8*4($sp) sw $9, 9*4($sp) sw $10, 10*4($sp) sw $11, 11*4($sp) sw $12, 12*4($sp) sw $13, 13*4($sp) sw $14, 14*4($sp) sw $15, 15*4($sp) sw $24, 16*4($sp) sw $25, 17*4($sp) sw $28, 18*4($sp) sw $30, 19*4($sp) sw $31, 20*4($sp) mfhi $3 #Mar.8.2008 乗除算レジスタ HI の待避 mflo $4 #Mar.8.2008 乗除算レジスタ LO の待避 sw $3, 21*4($sp) #Mar.8.2008 HI sw $4, 22*4($sp) #Mar.8.2008 LO #ここで C 割り込みルーチンを呼ぶ jal interrupt # C メインに飛ぶ lw $3, 21*4($sp) #Mar.8.2008 lw $4, 22*4($sp) #Mar.8.2008 mthi $3 #Mar.8.2008 乗除算レジスタ HI の復元 mtlo $4 #Mar.8.2008 乗除算レジスタ LO の復元 lw $1, 1*4($sp) lw $2, 2*4($sp) lw $3, 3*4($sp) lw $4, 4*4($sp) lw $5, 5*4($sp) lw $6, 6*4($sp) lw $7, 7*4($sp) lw $8, 8*4($sp) lw $9, 9*4($sp) lw $10, 10*4($sp) lw $11, 11*4($sp) lw $12, 12*4($sp) lw $13, 13*4($sp) lw $14, 14*4($sp) lw $15, 15*4($sp) lw $24, 16*4($sp) lw $25, 17*4($sp) lw $28, 18*4($sp) lw $30, 19*4($sp) lw $31, 20*4($sp) addi $sp, $sp, 25*4 mfc0 $27,$14 # EPC=14 戻り番地が記録してあるEPCを$27に読む jr $27 # 割り込み復帰 rfe # 割り込みEnable、ユーザモード復帰 nop .set reorder .end entry
Cでinterrupt() という名前の関数を定義すると、そこに割り込み時飛んできますので、そこにユーザ処理を書きます。未だモニタを書いていないのですが、例えば、次のような感じです。
void interrupt() { int c; c=read_uart();//UARTを読み込んで cbuf[counter]=c;//バッファにセーブ }
ところで、UARTのボーレートは、速くても115.2kbpsなので、これに足を引っ張られてシミュレーションに時間がかかってしまいます。そこで、RTLシミュレーション時は、define.hで以下のようにカウント数を(よく言うと)アクセラレートしています。仮想数MbpsのUARTになります。 論理合成時は、現実の世界に戻さないといけません。
このようにヘッダファイルで定義しておいて送信モジュール受信モジュール共同じヘッダファイルを参照するようにすればdefine定義のOn/Offだけでつじつまを合わせられます。
`ifdef RTL_SIMULATION `define FAST_SIM `endif `ifdef FAST_SIM `define COUNTER_VALUE1 (18) // TEMP 216) `else `define COUNTER_VALUE1 (216) // TEMP 216) `endif
さて、以上の準備を経てようやく、メインのCテストプログラム(電卓プログラム)です。今回は、newlibも整備してあるので少しいじれば浮動小数も使用可能です。
#include "yacc_port.h" #define BUF_SIZE 1000 unsigned char buffer[BUF_SIZE]; unsigned char * read_ptr=buffer; char result_buffer[16];//8+1 unsigned char sym; unsigned char* char_ptr; long term(void); long factor(void); long expression(void); void calculator(); int volatile int_flag=0;//volatile は必須。ないとcalculatorに行かない char buf[2]; #undef print_char void print_uart(unsigned char* ptr)// { unsigned int uport; #define WRITE_BUSY 0x0100 while (*ptr) { do { uport=*(volatile unsigned*) uart_port; } while (uport & WRITE_BUSY); *(volatile unsigned char*)uart_wport=*(ptr++); } //*(volatile unsigned char*)uart_wport=0x00;//Write Done } void putc_uart(unsigned char c)// { unsigned int uport; do { uport=*(volatile unsigned*) uart_port; } while (uport & WRITE_BUSY); *(volatile unsigned char*)uart_wport=c; } char read_uart()//Verilog Test Bench Use { unsigned uport; uport= *(volatile unsigned *)uart_rport; return uport; } void pp(unsigned char* ptr)//Verilog Test Bench Use { while (*ptr) { *(volatile unsigned char*)print_port=*(ptr++); } *(volatile unsigned char*)print_port=0x00;//Write Done } void print(unsigned char* ptr)//Verilog Test Bench Use { // pp(ptr); print_uart(ptr); } void print_char(unsigned char c) { // pp(&c); print_uart( &c); } void getsym() { while ( *char_ptr==' ' || *char_ptr=='\n' || *char_ptr=='\r' ) char_ptr++; if (*char_ptr ==0) { sym=0; }else { sym=*(char_ptr++); } } inline void init_parser() { char_ptr=buffer; getsym(); } long evaluate_number(void) { long x ; x=sym-'0'; while(*char_ptr >='0' && *char_ptr <='9') { x = x * 10 + *char_ptr - '0'; char_ptr++; } getsym(); return x; } long expression(void) { long term1,term2; unsigned char op; op=sym; if (sym=='+' || sym=='-') getsym(); term1=term(); if (op=='-') term1=-term1; while (sym=='+' || sym=='-') { op=sym; getsym(); term2=term(); if (op=='+') term1= term1+term2; else term1= term1-term2; } return term1; } long term(void) { unsigned char op; long factor1,factor2; factor1=factor(); while ( sym=='*' || sym=='/' || sym=='%'){ op=sym; getsym(); factor2=factor(); switch (op) { case '*': factor1= factor1*factor2; break; case '/': factor1= factor1/factor2; break; case '%': factor1= factor1%factor2; break; } } return factor1; } inline long parse_error() { print_uart("\n parse error occurred\n"); return 0; } long factor(void) { int i; if (sym>='0' && sym <='9') return evaluate_number(); else if (sym=='('){ getsym(); i= expression(); if (sym !=')'){ parse_error(); } getsym(); return i; }else if (sym==0) return 0; else return parse_error(); } /* 文字列の並びを逆順にする */ char *strrev(char *s) { char *ret = s; char *t = s; char c; while( *t != '\0' )t++; t--; while(t > s) { c = *s; *s = *t; *t = c; s++; t--; } return ret; /* 並べ替えた文字列の先頭へのポインタを返す */ } /* 整数を文字列に変換する */ void itoa(int val, char *s) { char *t; int mod; if(val < 0) { *s++ = '-'; val = -val; } t = s; while(val) { mod = val % 10; *t++ = (char)mod + '0'; val /= 10; } if(s == t) *t++ = '0'; *t = '\0'; strrev(s); } void strcpy(char* dest,char* source) { char* dest_ptr; dest_ptr=dest; while(*source) { *(dest++) =*(source++); } ; *dest=0;//Write Done } void calculator() { long result; //パーサ初期化 init_parser(); //計算 result=expression(); itoa(result,result_buffer); //結果表示 print_uart(buffer); putc_uart('='); print_uart(result_buffer); putc_uart(0x0a); putc_uart(0x0a); putc_uart(0x0d); } void main() { //オープニングメッセージ表示 putc_uart(0x0a); putc_uart(0x0d); print_uart("Welcome to YACC World.Jul.15.2004 www.sugawara-systems.com"); putc_uart(0x0a); putc_uart(0x0d); print_uart("YACC>\n"); //無限ループ while(1) { if (int_flag){//入力終了なら int_flag=0; calculator();//計算させて print_uart("YACC>");//プロンプトを表示 } } } void interrupt() { int c; c=read_uart();//uart read port から1バイト読み込み if ( c == 0x0a || c==0x0d ) { *read_ptr = 0;//string 終端 read_ptr=buffer;//read_ptr 初期化 putc_uart(0x0a); putc_uart(0x0d); if (int_flag) pp("PError!\n"); else int_flag=1; } else if ( c == '\b' && read_ptr > buffer ){//バックスペース処理 putc_uart('\b'); read_ptr--; }else if ( read_ptr>= buffer+BUF_SIZE){// overflow *read_ptr = 0;//string 終端 read_ptr=buffer;//read_ptr 初期化 print_uart("Sorry Overflow..!\n"); }else {//ポインタインクリメント putc_uart(c);//エコーバック *(read_ptr++) = c; } }
FirstTry論理合成結果
論理合成と配置配線にISEで約1時間かかりました。駆動可能周波数は、30MHz弱です。ロジックの使用率は70%位ですが、RAMは、100%使い切ってしまいました。 これだと肝心のDDRコントローラIPが載らなくなってしまいますね。 SRAMは、なるべくなら32KBにしたいので、UARTのFIFOとキャッシュをケチるしかなさそうです。
ポストレイアウトシミュレーション
レウアウト後の配線遅延情報をバックアノテートします。ネットリストは20万行,10MBのファイルで、コンパイル/シミュレーションは、RTLの数十倍の時間がかかります。デバッグは下流になればなるほど、困難になるので、論理的なバグは、RTLの段階でつぶしておきたいですね。
40ns(25MHz)で駆動してみました。問題ないようです。
実仕様に合わせる
今までは、シミュレーションで行いましたが、実際のFPGAに組み込むには、合成情報からフィードバックする必要があります。
1.駆動周波数
駆動可能な周波数は、30MHz弱というレポートでした。スタータキット上は、50MHzのOSCが載っていたので、分周するかDCMを使って25MHzにします。
2.実定数
UARTは、シミュレーション用の定数になっていました。1.で決まった駆動周波数に合わせてボーレートが115.2Kbpsの整数倍になるようにカウンタ定数を合わせます。
3.RAMの見直し
RAMの余裕が全くないことが分かったので、キャッシュサイズを見直します(シミュレーションでは、I/D共1KB)。
4.制約ファイル
今までは、ルータ任せのピン配置でしたが、スタータキット上のピン配置にする必要があります。
動かしてみる
とりあえず、DCMは使わずに1/2分周で動かしてみました。ボーレートは、57.6kbps、キャッシュは、I/D共512B、SRAMは32KBにしています。制約ファイルは、ピン配置のみです。
次は、HDLソースのDefine.h で、RTL_Simulationをコメントアウトして合成用にしています。
//`define RTL_SIMULATION //comment out for synthesis `define NEW_YACC //`define USE_VIRTUAL_PRINT_PORT //Virtual Port for Debug Use `define Print_Port_Address 32'h1f_fff0 // `define Print_CAHR_Port_Address 32'h1f_fff1 `define Print_INT_Port_Address 32'h1f_fff2 //First ADDRESS `define Print_LONG_Port_Address 32'h1f_fff4 //First ADDRESS `define Print_putchar_port_address 32'h1f_ffe0 //char port address `define FILE_NAME_READ_PORT_ADDRESS 32'h1f_ffc0 // I: FILE_NAME `define FILE_NAME_WRITE_PORT_ADDRESS 32'h1f_ffc4 // I: FILE_NAME `define FILE_IO_PORTS 32'h1f_ffc8 // 6 ports `define FILE_IO_START 32'h1f_ffc0 `define MAX_FILE_DESCRIPTOR 7 `define MIN_FILE_DESCRIPTOR 2 `define NUM_OF_FILE_DESCRIPTORS (`MAX_FILE_DESCRIPTOR-`MIN_FILE_DESCRIPTOR+1) `define UART_PORT_ADDRESS 32'h1f_fffc //Reserved `define INTERUPPT_ADDRESS 32'h1f_fff8 //Reserved `define EOF (-1) //UART_PORT // { 23'b Reserved, WRite_BUSY, read_data/write_data} ;// dpends on R/W `ifdef RTL_SIMULATION `define FAST_SIM `endif `ifdef FAST_SIM `define COUNTER_VALUE1 (22) // TEMP 216) `else `define COUNTER_VALUE1 (216) // TEMP 216) `endif `define COUNTER_VALUE2 (`COUNTER_VALUE1*2+1) `define COUNTER_VALUE3 (`COUNTER_VALUE1+3) //Compile Options `define IO_SPACE 16'h001f //001f_xxxx 64KB space None-Decached Domain `define Reset_Vector 0 //Reset Vector `define RESET_ADDRSS `Reset_Vector `define UTLB_Exception 32'h0000_0080 //Reserved `define General_Exception 32'h0000_0080 //Interrupt Vector `define ID_CACHE_SIZE (16) //512->16KBytes for each Icache/Dcache Min.=8,16,32....limitted by Vavlue of IO_SPACE //512 -16KB //258 -8KB //128 -4KB //64-2KB //32-1KB //16-512Byte
結果、ブロックRAMに余裕ができました。
ターミナルソフトを起動させてシリアルポートを57.6kbpsで接続します。Spartan3EキットのEastボタンを押してとリセットさせ、Welcom..がでれば成功です。
適当に式を入力してCRすると、
入力した式=答え
を返します。プロンプトを出しユーザ入力待ちになります。
H/WソースはUARTのmodule仕様に合わせて、割り込み入力をエッジセンスにした他は、前回ソースから変更ありません。
(遊んで見たい方用のbit ファイルです。)
DDRコントローラテスト用のモニタは、この電卓ソースを改造して作成したいと思います。イメージ的には、
YACC>W xx // ライトコマンド + パラメータ
結果表示 .
YACC>D xxx //ダンプコマンド +パラメータ
結果表示
..
のような感じです。
キャッシュ付き新版YACCが動いたので、IPコア評価に戻りましょう。
DDR IPのタイミング評価
RTLの評価が、十分ではないのですが、単品で合成、レイアウト後のシミュレーションを行いました。(IP内のH/W記述は少しだけ変更しています。) テストベンチは、前掲のベンチをそのまま使用しました。
<コラム>
いわゆる”HDLによるトップダウン設計”とは、この事を指しています。RTLは、組み合わせ回路の遅延を0にしたモデリングです。それを論理合成で、ゲートレベルのオブジェクトに置き換えました。このときテストベンチは、RTLで使用したものをそのまま使うことに着目してください。設計の様子は、この長いホームページを見るとボトムアップで行っていることが分かると思います。しかし、RTL->ゲートへの置き換えは、正にトップダウンです。RTLシミュレーションでは、机上で何回、否、何百回となく、シミュレーションを行って論理的なバグを修正しながら進みました。ご承知のように、このステージでは、記述によっては、設計者の意図と異なる回路が合成されてしまう危険があります。それにも拘わらず、なぜゲートでなくてRTLなのかは、もうお分かりですね。単純に、シミュレーションが速いからです。
RTL環境ならCのソースコンパイルからHDLコンパイル、シミュレーション開始まで数秒ですが、実機では、この程度の規模で、1時間位かかってしまいます。
さて、結果、IFD_DELAY_VALUEは、1から4まで動作することが分かりました。設計者の指定は、2でしたから、ほぼ狙い目通りになっていると言ってよいでしょう。
以下では、IFD_DelayValueは、0で、DDR_CLKの位相を少しだけいじって動かしています。なお、IFD_DelayValueを指定すると、同じUCF指定でも階層の具合で、Delayの入り方が大幅に違うことがあるようなので注意します)
評価用ポートの作成
YACCはRTL/遅延シミュレーション/実機、IPはRTL/遅延シミュレーションで動いたので、次の作業は両者をドッキングさせることです。評価用のソフト自身をDDRに載せると、動かない場合に訳が分からないことになるので、ここは、分離して慎重に一歩づつ進みます。CPUからW/RできるIOポートを非キャッシュ領域に作りそこで、実機での評価を行うことにします。
CPUは、25MHz、DDR IPは、100MHzで動くのでクロックの乗り換えが必要です。ここで注意するべきは、同じ位相で生成したとしても、スキューは、保証されないことです。ここでのミスは、RTLシミュレーションでは発見されないので注意を要します。(一般に非同期箇所の接続は、シミュレータやLINTツールは理解していませんし、ASICが上がっきての多くのトラブルはこの辺にあります。)ここでは、単純に位相をずらす方式にしました。つまり25MHzは、100MHの逆位相を使用します。 位相がずれた同期であれば、メタステーブルの心配がなく、非同期遅延だけチェックすればよいので簡単です。 また、段間は、FFで受ける仕様としました。レジスタ仕様は、以下です。YACCのアクセスは、ライトバックキャッシュ方式であり4ワードバーストアクセスのみしかないのですが、ここでは最大8ワードとしました。また、DMAは搭載予定がありません。長いバーストについては、実機評価を行いません。
名称 | W/R | ビット | ビットフィールド名 | 内容 |
DDR RW Address | W/R | [31:0] | ddr_address | DDRをR/Wするアドレスを設定 |
DDR_Control | W | [0:0] | R_W_direction | R/W を指定 Writeは1、Readは、0を指定する。本ワードの書き込みでコマンドスタート指示になる |
R | [0:0] | Busy | Command Processing: 1のときコマンド実行中(Busy)。1が立っているとき、このグループのレジスタは書込み禁止、Reset直後は、Busy | |
W | [4:1] | N_of_words | NoOfWords : 最大8ワード(1ワードは、4バイト)までのバーストW/R数を指定, リード時は、不定 | |
- | [31:5] | Reserved | ||
DDR_Data0 | W/R | [31:0] | dreg[0] | R/Wされる最初のワード |
DDR_Data1 | W/R | [31:0] | dreg[1] | |
DDR_Data2 | W/R | [31:0] | dreg[2] | |
DDR_Data3 | W/R | [31:0] | dreg[3] | |
DDR_Data4 | W/R | [31:0] | dreg[4] | |
DDR_Data5 | W/R | [31:0] | dreg[5] | |
DDR_Data6 | W/R | [31:0] | dreg[6] | |
DDR_Data7 | W/R | [31:0] | dreg[7] |
CPUは、上記ポート仕様でコマンドを書き込み、結果を待ち、コンペアチェック等を行います。
以下、コマンド仕様です。
コマンド | 内容 | コマンド書式 | コマンド例 |
ダンプ | 指定DDRアドレスのDDRメモリをダンプ表示する | D アドレス[HEX] 8ワードを単位とした単位数(HEX) | D 0 10 |
シングルワードライト | 指定DDRアドレスに指定値をDDRに1ワード書く。書いた後に読み、Verified=で表示される。(4BYTE BOUNDARY ONLY) | W アドレス[HEX] データ[HEX] | W 0 DEADBEAF |
シングルワードリード | 指定DDRアドレスのデータを読む (4BYTE BOOUNDARY ONLY) | R アドレス[HEX] | R 0 |
レジスタライト | 指定アドレスのIOポートをライトする | S アドレス[HEX] データ[HEX] | S 1FFB4 11 |
レジスタリード | 指定アドレスかIOポートをリードする | ? アドレス[HEX] | ? 1FFFB4 |
インクリメンタルライト | インクリメンタル ライト データとしてアドレス値を書く | I アドレス 8ワードを単位とした単位数(HEX) | I 0 100 |
インクリメンタルリードコンペア | インクリメンタル リード アドレス値とコンペアを行う | C アドレス 8ワードを単位とした単位数(HEX) | C 0 100 |
ドッキング後のRTLシミュレーションです。重たいDCMが3個も入っているのでRTLにもかかわらずとても遅いです。
遅延シミュレーションの様子です。ほんの30数マイクロsecのシミュレーションですが、結構な時間がかかっています。
実機(Sprtan3E starter-kit)でのテストの様子です。Single W/Rで100万パスやってみました。(テラタームのマクロで動作させています)
このテストは、同時にYACCの割り込みのチェックにもなりました。
.
一晩寝かせた(電源オンのままアクセスしないで)後、Read&Compare しました。エラーは発生しませんでした。
8ワードバーストアクセスで、Write Once後、数百テラビット リードコンペアしてNo Errorでした。
下は、コマンドの例です。
これで今回の評価はおしまいです。(3/23/2008記)
他のDDRコントローラIPリンク | Remarks | |
1 | 犬山回路研究所 | spartan 3E スタータキット用。Verilog HDL.制約の書き方、Primitiveの指定の仕方が参考になります。 |
2 | Wishbone仕様 | spartan 3Eスタータキット用。Verilog HDL. Wishboneバス直結 micro32につなぐプロジェクトも進行中のようです。 |
将来的なプラン
OSを動かすための準備をする
=>或いは、コンパクトフラッシュという手があるかもしれません。こちらの方は、数本という訳には行かないですが、転送レートが高いので、より実用的かもしれません。IPとしてまとまっているものが公開されています:IDE コントローラ Shallot http://homepage3.nifty.com/~Natsutan/ide/shallot.html (シミュレータもVeritakを使われているので、ポートも簡単かもしれません。IDEというのは、昔のハードディスクのインターフェースです。)
SDカードのSPIについては、http://elm-chan.org/docs/mmc/mmc.html に詳しく書かれていてとても参考になります。(FATアクセスドライバも公開されています。)
実装は、当分時間が取れないので大分先になります。
Q.DWM付録 XC3S250Eに載るでしょうか?
はい。キャッシュ容量をいじれば、LUT数的には余裕です。SDカードを主メモリとする32ビットプロセッサは、面白いかもしれません。筆者も一つキープしておこうと思います。なにせPCと同じ位のメモリを積んでもこの価格、魅力です。(2年前の付録では、4KBで四苦八苦しました。)
OSのポート :なにかターゲットを絞ってMMUなしで走るOSをポートしてみたいと思います。
その先は?