はじめに

FPGAを使って何か作るとき、ソフトコアCPUを使いたいことがあります。
FPGAでPC-8001を作る計画では、Z80バイナリコンパチのコアを作りましたが、もっとシンプルで、遅くていいので、小さいやつが欲しいと思います。
フリーでは、例えばPIC互換のMini-Riscなどがありますが、PICのアセンブラは書きたくありません。
PicoBlazeなんかは小ささは芸術的ですが、Xilinx限定という意味で、フリーではありません。
Cは使えなくてもいいので、もっとこう、32ビットデータでも普通に扱える、アセンブラで優雅に書けるようなのがいいと思います。
かといって、MIPS互換とかでは大きすぎます。

そこで、自分で作ることにしました。
もっとちゃんと探してもいいんですが、せっかくFPGAが使えるようになったので、自分オリジナルのCPUを作ってみたいというのもあります。
最近の情報系の学生はそういう授業があるそうですね。うらやましい。しかも、ちゃんと買うとものすごく高価なすごい論理合成ツールが使えたりするらしい。いいなあ。
まあそれはそれとして、インストラクションセットなどはMIPSを参考に、しかし可能な限り小さくするため、ALUは1ビットとします。
つまり、「1ビットCPU」です。

1ビットCPUといえば汎用CMOSの中にMC14500というのがありました。これ単体ではCPUとは言えませんが。
今回は、表面的にはもっと普通のCPUに見えるものにします。

レジスタも1ビットでは使いものにならないので、ブロックRAMを使うことを前提に、豪華に64ビット長にします。64ビット長の加算をするときは1ビットずつ、64回ループします。ループはハードウェアで行われ、アセンブラでは単に64ビット長のADD命令です。

すごく遅そうです。使いものになるんでしょうか?
例えば、8ビットの加算を考えます。1ビットについて、2回読んで1回書く、を8回繰り返すので、24クロック+αかかります。αの部分は実行開始前のセットアップです。
50MHzで動かすとして、1回の加算に0.5μSくらいかかります。
まあ、PICの代わりくらいにはなりそうなので作ってみることにします。
FPGAで使うことが前提なので、どうしても遅い処理はハードウェアとして記述してI/Oにつなげば済みますから。

基本構成

まず、プログラムメモリとデータメモリを分離します。
プログラムメモリは、とりあえずブロックRAMを使います。
データメモリはブロックRAMでも外付けのRAMでも、あるいは無くてもいいことにします。
データ幅は、扱うデータが8ビット単位なので余分なセレクタなどが不要な8ビットとします。
I/Oはデータメモリ空間に割り付けます。
ALUは1ビットなので、レジスタに使うブロックRAMはデータ幅が1ビットと8ビットのデュアルポートにします。
それを中心にF/Fやセレクタをつければ出来上がりです。

レジスタセット

汎用レジスタは64ビット長にします。データ長としては8/16/32/64ビット、符号付き/符号無しデータが扱えるようにします。レジスタ長より短いデータを扱うときは、未使用の上位部分は不変です。
レジスタの本数は、32本あれば十分でしょう。
64ビット×32本だと2kビットになります。とりあえずのターゲットはSpartan3で、これのブロックRAMは18kビット単位なので、だいぶ余ります。なので、レジスタバンク方式にします。
全体を切り替えるとすると8バンクですが、全体が切り替わっても使いにくいので、32本を前半と後半に分け、それぞれをローカルレジスタ、グローバルレジスタという扱いにします。
16本だと16バンクになりますが、そのうち2バンクをグローバルレジスタに割り当て、通常時と割り込み時に自動的に切り替わるようにします。これにより、スタックポインタをグローバルレジスタ上に取れば、ユーザスタックと割り込みスタックを分けられます。
残りの14バンクをローカルレジスタに割り当て、BANK命令で切り替えられるようにします。サブルーチンの先頭でそのルーチンで使うバンクに切り替え、リターンするときに復帰します。
このプロセッサは小規模のプログラムをアセンブラで書くことを想定しているので、ローカルレジスタだけをワークに使う分にはスタックは不要かもしれません。 他に、専用レジスタとしてPC(プログラムカウンタ)、フラグがあります。

インストラクションセット

インストラクションのビット幅は、オペランドにレジスタを3つ取ると32ビット、2つだと16ビットというところですが、ケチって16ビットにします。
32本のレジスタを2個指定すると10ビット必要で、サイズ指定に2ビット取ると、残りは4ビットです。その4ビットでオペコードを指定することにします。
サイズ指定は、以下のようにします。
.b  8ビット
.s  16ビット
.l  32ビット
.ll 64ビット
結局、以下のようなインストラクションセットにしました。
データ移動
LI.<サイズ指定> <dstレジスタ>,<定数式> 0011 0000 0ddd dd00 0000 0000 cccc cccc 0011 0000 0ddd dd01 cccc cccc cccc cccc 0011 0000 0ddd dd10 cccc cccc cccc cccc cccc cccc cccc cccc 0011 0000 0ddd dd11 cccc cccc cccc cccc cccc cccc cccc cccc cccc cccc cccc cccc cccc cccc cccc cccc 定数をロード
LD.<サイズ指定> <dstレジスタ>,[<adrレジスタ>] 0100 aaaa addd ddSS データメモリからのロード
LDC.<サイズ指定> <dstレジスタ>,[<adrレジスタ>] 0101 aaaa addd ddSS プログラムメモリからのロード
ST.<サイズ指定> <srcレジスタ>,[<adrレジスタ>] 0110 aaaa asss ssSS データメモリへのストア
MOV.<サイズ指定> <dstレジスタ>,<srcレジスタ> 1000 ssss sddd ddSS レジスタ間移動
演算
ADD.<サイズ指定> <dstレジスタ>,<srcレジスタ> 1001 ssss sddd ddSS dst←dst+src
SUB.<サイズ指定> <dstレジスタ>,<srcレジスタ> 1010 ssss sddd ddSS dst←dst-src
CMP.<サイズ指定> <dstレジスタ>,<srcレジスタ> 1011 ssss sddd ddSS dst-srcの結果をフラグにのみ反映
OR.<サイズ指定> <dstレジスタ>,<srcレジスタ> 1100 ssss sddd ddSS dst←dst|src
XOR.<サイズ指定> <dstレジスタ>,<srcレジスタ> 1101 ssss sddd ddSS dst←dst^src
AND.<サイズ指定> <dstレジスタ>,<srcレジスタ> 1110 ssss sddd ddSS dst←dst&src
TST.<サイズ指定> <dstレジスタ>,<srcレジスタ> 1111 ssss sddd ddSS dst&srcの結果をフラグにのみ反映
以下の7命令はアセンブラによってr0への定数ロードとレジスタ間演算に展開されます。 ADDI.<サイズ指定> <dstレジスタ>,<定数式> SUBI.<サイズ指定> <dstレジスタ>,<定数式> CMPI.<サイズ指定> <dstレジスタ>,<定数式> ORI.<サイズ指定> <dstレジスタ>,<定数式> XORI.<サイズ指定> <dstレジスタ>,<定数式> ANDI.<サイズ指定> <dstレジスタ>,<定数式> TSTI.<サイズ指定> <dstレジスタ>,<定数式>
シフト
SV.<サイズ指定> <dstレジスタ>,<paramレジスタ> 0111 pppp pddd ddSS 2番目のレジスタは、シフトパラメータを指定します。
このプロセッサのシフト命令はちょっと変わっています。レジスタから1ビットずつ読み出してコピーするとき、ソースアドレスをビット単位で与えることで、結果的にシフト操作になる仕組みです。シフトにかかる時間は通常の演算と同じくデータサイズだけに依存し、シフトするビット数に依存しません。
左シフトのとき
(制限)srcとdstは同じレジスタを指定できない。
 シフトパラメータ = 64 * srcレジスタ番号 - シフト数
符号なし右シフトのとき
 シフトパラメータ = 64 * srcレジスタ番号 + シフト数 | 0x8000
符号つき右シフトのとき
 シフトパラメータ = 64 * srcレジスタ番号 + シフト数 | 0xc000
ビットアドレスのカウンタはインクリメントしか用意しないので、コピーする領域が重なる左シフトでは同じレジスタを指定できません。
以下の3命令はアセンブラによってr0へのシフトパラメータロードとSV命令に展開されます。
SLL.<サイズ指定> <dstレジスタ>,<srcレジスタ>,<定数式> (制限)srcとdstは同じレジスタを指定できない。 SRL.<サイズ指定> <dstレジスタ>,<srcレジスタ>,<定数式> SRA.<サイズ指定> <dstレジスタ>,<srcレジスタ>,<定数式>
ジャンプ/コール
J <ラベル> 0001 1000 0000 0000 llll llll llll llll 無条件にジャンプ
JNZ <ラベル> 0001 1000 0010 0000 llll llll llll llll Zフラグが0のときジャンプ
JZ <ラベル> 0001 1000 0010 1000 llll llll llll llll Zフラグが1のときジャンプ
JNC <ラベル> 0001 1000 0011 0000 llll llll llll llll CYフラグが0のときジャンプ
JC <ラベル> 0001 1000 0011 1000 llll llll llll llll CYフラグが1のときジャンプ
CALL <ラベル> 0001 1100 0111 1100 llll llll llll llll PCと状態をr31にコピーし、ラベルにジャンプ
RET 0001 0110 0111 1100 r31をPCと状態にコピー
RETI 0001 0100 0111 1100 r31をPCと状態にコピー。割り込みからの復帰専用
JR <レジスタ> 0001 0000 0rrr rr00 指定レジスタをPCにコピー
その他
BANK <レジスタバンク> 0010 0000 0000 bbbb レジスタバンクの設定値は0-13。14と15は特殊。 14 グローバルレジスタのノーマルバンクをローカルレジスタにマップ 15 グローバルレジスタの割り込みバンクをローカルレジスタにマップ
NOP 0000 0000 0000 0000 何もしない

実装

fz80では、TTLで組むかのような勢いでゲートレベルに近い記述をしました。
この書き方は、回路は小さくなるのですがロジックの段数がかなり増えて、FPGAでは遅くなってしまうようです。
そこで今回は、プログラムっぽい書き方にしてみました。
とは言っても、一つのalwaysブロックで複数のレジスタを書き換えると回路が大きく、遅くなりがちなのでなるべく個別のレジスタに分けました。
ISEWebpack7.1iでXC3S200に実装した結果、185(エリア優先)〜218(スピード優先)スライスと、思ったより大きくなってしまいました。
調べてみると、同期ロードつきUPカウンタで済むはずのプログラムカウンタがレジスタ+アダーになっていたり、最適とはいえない論理合成になっています。
一方、スピードの方は良好で、kp1単体で配置配線後の最小周期は11.8(スピード優先)〜12.9(エリア優先)nsでした。
現状、フリーで使える論理合成ツールで回路規模とスピードを両立するには、PicoBlazeのようにプリミティブの羅列で、アセンブラ的に書かざるを得ないのかもしれません。

アセンブラとサンプル

簡易アセンブラをperlで書きました(askp1)。簡易とはいえ、ローカルラベルとレジスタネーム機能を入れてあります。
普通のperlでは64ビットの値は扱いにくいため、LI命令のサイズ.llのときは16進リテラルのみ対応しています。
アセンブラが吐くバイナリは、ucfのテンプレートにINITを追加することでブロックRAMを初期化する方式でロードします。今のところ、1ブロック(1kワード)限定です。
サンプルは、マイコンのサンプルとして定番のナイトライダーです。スパ3スターターの左下の8個のLEDで、ナイトライダーします。ソフトウェアによるPWM制御で尾を引くようにしました。



download(kp1.tar.gz)

このソースは、非営利目的に限り自由に使用できます。
ただし、いっさいの保証もサポートもありません。