2011年12月23日金曜日

独自のCPUを創ってみる(10) - TD16


独自のCPUを創ってみる(9) - まとめ」の続き



TD32を創ってみて、CPUの設計方法が少しは分かった。
spartan-3 では、1個のスライスで最大 2:1 MUXしか実装できない。
4:1 MUXだとスライスが2個必要になる。
だから、32bit の 4:1 MUX を実装するには64スライス必要になる。
たった4種類の演算をするALUだけでこれだけのリソースが必要になる。
ところが pico blaze はCPU全体で96スライスだ。
だから、データ幅の広い信号のMUXは慎重に使わねばならない。
それと、メモリの使い方も重要なポイントだ。
TD32では単純にフラップフロップで複数のレジスタを実装したが、
これがリソース消費を拡大した。これは論外だった。
複数のレジスタは分散RAMかブロックRAMを使わねばならない。
どちらも高速なSRAMで、しかもデュアルポートの構成にできるから、
例えばパイプライン2ステージにして、それぞれのステージでRAMに
アクセスできる。pico blaze では16個の任意のレジスタ間で
sx <= sx + sy + carry
のような演算ができる。デュアルポートRAMをうまく使う必要があるだろう。

よく見かける、CPUのブロック図の見方も分かってきたような気がする。
データ幅の広い信号の流れに着目し、またMUXの使い方にも注意して
見るといいと思った。ブロック図にMUXが描かれているのは、
リソース消費や動作速度に大きく影響するからだろう。

改めて、16bit のCPUを作ってみた。
まだデバッグしてないのでまだまだバグがあると思うが。
注目したいのは動作速度とリソース消費だ。
現時点で最高速度 82.291MHz 、周辺回路含めて 142 スライスだった。

アーキテクチャは三段のパイプラインで、

(1) 命令アドレス計算、命令フェッチ、PC更新
(2) メモリアドレス計算、メモリ読み込み又は書き込み
(3) ALU実行、結果をアキュムレータに格納

という流れ。(1)と(2)ではデュアルポートのブロックRAMを同時にアクセスする。
アキュムレータは1つしかなく、フリップフロップで構成する。
パイプラインの制御はまだ実装してない。
(2)でブロックRAMから読み込んだ値をオペランドとしてALUを実行すると、
これだけで10nsecを超えてしまい、どうしても100MHz動作は無理なようだ。

高速化するためには、ALUのオペランドや結果の格納を分散RAMに
限定するしかなさそうだ。そうするとpico blazeと似てくるが、
pico blaze は2クロックで1命令実行だ。TD16 ではパイプラインで
なんとかして1クロックで1命令実行できるようにして、しかも16bit CPUと
することで差別化したい。当然pico blazeよりリソースを消費するだろうけど。

2011年12月22日木曜日

独自のCPUを創ってみる(9) - まとめ


独自のCPUを創ってみる(8) - DMMファイルの続き


未完成のTD32のプロジェクトファイル

拾い物のDDR SDRAMコントローラを試す

で試したSDRAMコントローラとTD32を繋ぎたいと、やってみたがうまく行かない。
原因は2点考えられ、一つはもともとこのコントローラはちょっと変、
ということと、もう一つはSDRAMコントローラは100MHz用で、
一方CPUのTD32はそんなに高速に動作しないので25MHzで
動かしていて、ちょうど4倍のクロックなのを利用してなんとか
接続する回路にしたはずなのだが、まだどこかにバグがあるかも知れない、
ということ。

コントローラが変なのは、もともとこれを作った人がコメントで
以下のようなことを書いてて、つまりうまくタイミングが合ってないのだ。

/***************************************************
 * De-duplex of the data path
 * For some reason, for read, the output is CL=2.5, not CL=2
 * Thus, catching of the data is 1/2 a cycle late.
 * Flip the main_clk and main_clk_n, 1/2 cycle delay catched
 * on data_mux_latch
 ***************************************************/

がんばれば、いつかはバグが取れるかも知れないが、
だれてきたので、ここで一旦締めて、別のテーマに移ることにする。
でも「独自CPUにDRAMを接続する」というのは諦めてないので、
いつかまた戻ってくるだろう。

もともと、以下の目的があった。


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

反省として、TD32はアーキテクチャというものを考慮しておらず、
そのため動作周波数が遅い上にリソース消費が大きいことだ。
単純に以下のようなレジスタを定義しているが、この部分で
512個のフリップフロップを使ってしまっている。

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

また、以下のような32bitのMUXを多用していて、
このような記述でかなりリソースを使ってしまっている。

wire [31:0] alu2 = (cycrd | cycrd2) ? indata1 : 
(oprr | opar) ? grim8 : gr[opregj];

また、インタフェース仕様としてwishboneを使うこと自体は
よかったが、CPUが1クロックで命令を実行するというのに、
その1クロックの忙しい間にwishboneでI/OやRAMにアクセス
するのはぜんぜん無理だった。
次に作るときは、wishboneはI/O用とDRAM用で別にするか、
もしくはI/Oだけwishboneで接続して、DRAMは別途専用配線と
するのがいい。
今回はメインメモリもwishbone接続だったが、メインメモリは
BLOCK RAMをうまく使ってCPU内部の専用配線にしないと
タイミング的に間に合わない。また、レジスタファイルも
BLOCK RAMを活用しないとリソース的にも問題だ。

これらの反省を生かして、次は100MHz動作の16bit CPUを創ってみたい。

さて、最後に備忘録として各種ファイルの説明をしておく。
半年もすると絶対忘れてしまって自分でも分からなくなると思うので。

asm.cmd
TD32用のアセンブラを使ってアセンブルしてHEXファイルを生成する。
次にram1.bmm の記述に従って top.bit 内のBLOCK RAMを書き換え、
top_rp.bitを生成する。
data2mem -bm ram1.bmm -bd %MEM% -bt top.bit -p xc3s500e
次に、impactを使ってボードに書きこむ。
call wbit top_rp.bit

bus.v
wishboneバスを使ってCPU, RAM, I/O などを接続する。

Chattering.v
チャタリング除去用の簡単な回路

clkgen2.v
ddr_sdram.v で使われているDRAM用のクロック生成回路

dcm1.v
入力クロックを n/m 倍して任意のクロックを生成する

dcm2.v
90度ずつずれた4つのクロックを生成する

dcm4ddr.v
dcm1とdcm2を足したような回路

ddr_sdram.v
SDRAMコントローラ

hexconv.py
tool.py hex2hx2 に統合した

inport.v
汎用入力ポート

outport.v
汎用出力ポート

output.bmm
以下の出力例
data2mem -bm ram1.bmm -bd p2.mem -o p output

output.v
以下の出力例
data2mem -bm ram1.bmm -bd p2.mem -o v output

p1_led.txt
LEDを光らせるテストプログラム。ハンドアセンブルした。

p2.asm
p1_led.txt をアセンブラで置き換えた。

p3.asm
DRAMとの接続テスト用。結局、ちゃんと動作してない。

param.h
以下の定義を含むヘッダファイル。
シミュレーションのときは定義し、ボードに書きこむときはコメントアウトする。
`define DEBUG_SIM

paramconv.py
tool.py fmthex に統合した

paramlist_cpucyc.h
paramlist_opaddr.h
paramlist_opalu.h
paramlist_opcond.h
veritakでenum表示を使うための定義ファイル。
これらのファイルをincludeして、信号と紐付ける。
veritakでenum表示を選択すると、信号の値が数値ではなく
ラベルで表示される。

Prescaler.v
分周回路

prog.hex
prog.txtから、tool.py fmthex で整形した結果

prog.txt
ハンドアセンブルしてた頃のテストプログラム。
テストベンチt_top.v で使われ、多くの命令をひと通りテストする。

progconv.cmd
paramconv.py を起動するバッチファイル

ram.v
block ramを使用した512 word x 32 bit メモリ
wishbone 仕様

ram1.bmm
asm.cmd参照

rom.v
テストプログラムを含むROM。

td32.v
TD32 本体

td32asm.txt
TD32用のマクロ定義ファイル。汎用アセンブラAASMに適用する。
使用例はasm.cmd参照。
AASMのおかげで、簡単にアセンブラを利用できた。
ハンドアセンブルでずいぶん苦労したが、AASMがこんなに役に立つと
分かってたらもっと早くマクロ定義をしたのだが。

tool.py
各種のツールをまとめたもの。含まれるツールは下記。

tool.py fmthex [-h] [-f fillstring] [-s size] <in-file> [<out-file>]
    fmthex - format for $readmem(). "00_ab/*..*/cd" => "00abcd"

tool.py hex2hx2 [-h] [-f fillstring] [-s size] <in-file> [<out-file>]
    hex2hx2 - format for $readmem(). intel HEX (output of AASM) => HX2 (suitable for readmem())

tool.py hx22mem [-h] <in-file> [<out-file>]
    hx22mem - hx2 => xilinx mem file for data2mem command (to replace memory part of a bit file


top.ucf
UCFファイル

top.v
topファイル。クロック生成、リセット回路など。

top.vtakprj
top2.vtakprj
top3.vtakprj
veritak用プロジェクトファイル

t_top.v
t_top2.v
t_top3.v
テストベンチ

v13.gise
v13.xise
ISE用プロジェクトファイル

wbit.bat
IMPACTでボードに書きこむバッチファイル。
bitファイル名を省略すると、top.bitとなる。
wbit <bitファイル>

wishbone_wait.v
wishboneのslave側で、ACKの応答を固定クロック遅らせるための回路

以上

2011年12月3日土曜日

独自のCPUを創ってみる(8) - DMMファイル


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


Xilinx の data2memというコマンドを使うと、bitファイルをリコンパイルすることなく、
その中のblock RAMの初期化データだけを入れ替えることができる。

ISEの中からも使えるような記述があるが、うまく使えないようだ。
(ネットの書き込みによれば、プロジェクトファイルを作りなおせばいいらしいが)

コマンドベースで一応、うまく行ったのでメモっておく。

(1) ISEでbitファイルを生成する。

(2) FPGA Editor を起動し、書き換えたいblock RAMのインスタンス名とロケーションを確認する。

それには、Place&Routeの中の"View/Edit Routed Design" をダブルクリック。
FPGA Editorの右上のName filterのところに*myram* のように分かっている名前の一部を入力。
見つかったらそのインスタンスを選択して、F2を押す。
プロパティ画面のName欄がインスタンス名、Locationが配置されたロケーションだ。

(3) UCF ファイルに制約を追加する。(ロケーションが勝手に変わると不便なため)
先ほど確認したものを、以下のように入力する。
INST "bus/ram1/Mram_ram" LOC = "RAMB16_X1Y7";

(4) BMMファイルを作成する。
以下のように書く。UCFに書いたものと微妙に書式が違う。
コード: 
// comment...ADDRESS_SPACE ram1 RAMB16 [0x00000000:0x000007FF]BUS_BLOCKbus/ram1/Mram_ram [31:0] LOC = X1Y7;END_BUS_BLOCK;END_ADDRESS_SPACE;
最初のram1は、アドレススペース名。使わないので何でもいい。
RAMB16は使用しているブロックRAMの種類。
パリティも使うときはRAMB18を指定する。

[0x00000000:0x000007FF] は全体のメモリサイズ。
ワード幅に関係なく、バイト単位のサイズを指定する。
[31:0]の部分が、ワード幅を示している。今回は32bit * 512ワードの構成。
[7:0] と書けば、8bit * 2048 ワードと解釈される。

試してないが、もし2個使って32bit * 1024ワードにするなら以下のようになるはず。

コード: 
// comment...ADDRESS_SPACE ram1 RAMB16 [0x00000000:0x00000FFF]BUS_BLOCKbus/ram1/Mram_ram [31:0] LOC = X1Y7;END_BUS_BLOCK;BUS_BLOCKbus/ram2/Mram_ram [31:0] LOC = X1Y8;END_BUS_BLOCK;END_ADDRESS_SPACE;
BUS_BLOCK で囲まれた範囲は、一度に同時にアクセスするRAMを示している。
例えば同じサイズでも、並列にアクセスすることで64bit * 512 ワードにしたいときは
以下のようにする。これをハードウェアの構成と合わせないと、読み込まれる
データの配置と合わなくなる。

コード: 
// comment...ADDRESS_SPACE ram1 RAMB16 [0x00000000:0x00000FFF]BUS_BLOCKbus/ram1/Mram_ram [63:32] LOC = X1Y7;bus/ram2/Mram_ram [31:0] LOC = X1Y8;END_BUS_BLOCK;END_ADDRESS_SPACE;
なお、BMMファイルの拡張子は .BMM にすること。

(5) 入れ替えるMEMファイルを用意する。
以下のような形式の単純なテキストファイル。

@00000000
01234567
89ABCDEF
....

拡張子は必ず .MEM とすること。

一行目はアドレス指定。常に「@00000000」で良い。
二行目からは設定したいデータワード。
BMMファイルの最初の例では32bitワードなので、
8桁の16進数で一行ずつ並べる。
全部で513行になる。

(6) 以下のコマンドで bit ファイルを書き換える。


data2mem -bm ram1.bmm -bd test.mem -bt top.bit -p xc3s500e

ram1.bmm はBMMファイル、test.mem はMEMファイル、top.bitはISEで作成したbitファイル。
xc3s500e はFPGAチップの種類。以下のコマンドで名前の一覧が表示される。 

data2mem -h  sup

正常終了すると、top_rp.bit ができる。

2011年11月29日火曜日

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


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


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

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

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

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

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


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

2011年11月27日日曜日

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


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


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

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

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

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

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

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

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

あとやり残したことは、


2011年11月25日金曜日

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


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


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


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


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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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


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


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



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


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

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

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


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


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

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


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


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

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

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

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

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

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

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


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


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

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



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

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

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

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

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

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


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

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

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

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


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

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

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

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

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

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

18. バレルシフタ
⇒ ×

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

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

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

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

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

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


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

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

後日、よく検討しよう。