久しぶりにFPGAを使うと、オリジナルCPUを作ってみたくなります。
今回は、Cコンパイラが使えるちゃんとしたIPを使うほどでもない用途に、
小さくてアセンブラで書きやすいものにします。
方針
9ビットCPU
データ幅は9ビットにします。
BSRAMはパリティ用に9ビット幅にできますが、これを余すことなく使ってみようと。
例えば、Cライブラリのgetchar()のような8ビットのキャラクタを返す関数を作るとき、9ビットあればEOFも表現できます。
また、ファイルを扱う低レベルのルーチンではセクタ内オフセットが9ビットなので、好都合です。
汎用レジスタもアキュムレータも持たない
アセンブラを書くときは、それぞれの場所で各レジスタを何の用途に使っているか意識する必要があります。
コール・リターンで退避・復帰もあります。
そこで、思い切って無くしてみました。
6502のゼロページのように、データメモリをレジスタとして扱います。
サブルーチンごとに別々の場所を使えば、退避・復帰の必要はなくなり、ラベル名で用途も分かるように書けます。
欠点は演算命令で2つのオペランドを指定すると、命令のビット数が増えることです。
# スピードの問題もありますが、気にしません
今回は、命令長を32ビットにしました。
BSRAMを1個使う場合、データが32ビットだとアドレスは9ビットになります。
データ幅9ビットと相性がいいです。
小規模制御用と割り切ってプログラムカウンタも9ビットにすれば、コールでPCをストアするときに1回で済みます。
ハードウェアスタックも無し
ではコール・リターンはどうするのか。
PDP-8では、コールの次の命令のアドレスをサブルーチンの先頭アドレスに書き、その次のアドレスにジャンプします。
帰るときは先頭アドレスの値をPCにロードします。
今回はプログラム用とデータ用のBSRAMは分離するので、
アセンブラがサブルーチンごとにリターンアドレス用の領域をデータエリアに確保し、
コール・リターンではそこを参照するようなコードを自動的に出力するようにしました。
なお、リエントラント・リカーシブには対応しません。
データは符号無しのみ
小規模制御用だと、符号付きのデータを扱えなくてもなんとかなります。
これで符号フラグ、オーバーフローフラグとそれに伴う分岐命令が不要になります。
データサイズ8/9/16/24/32ビットをサポート
例えば、Z80では8ビットデータならAレジスタ、16ビットデータならHLレジスタを中心に処理します。
データサイズごとにコードを考える必要があります。
このCPUでは同一の演算命令でデータ幅を上記のサイズの中から選べるようにします。リトルエンディアンです。
複数バイトの加減算では最下位はキャリーなし、それ以降はキャリーありの命令を使うとか、
複数バイトの右シフトでは最上位でシフト、それ以降はキャリーを含めたローテートを使うとかします。
それらをプロセッサ内でループしながら自動的に処理する仕組みを作ります。
回路が増えてしまいますが、書きやすいアセンブラを目指します。
なお、複数バイトのときはデータは8ビット単位とします。そのほうが使いやすいので。
ポインタ
ではレジスタはPCとフラグだけ?
BSRAM一個で幅9ビットだと、アドレスは11ビットになります。
これをポイントするレジスタがあった方がメモリ操作しやすいので、srcポインタとdstポインタを用意しました。
(ここだけは)符号付きのオフセットを加算することができます。
ポストインクリメント、ポストデクリメントもできます。
え?プリデクリメントじゃないと使えないんじゃ?
回路を少しでもシンプルにするための仕様です。
このような場合もオフセットは有効なので、-1[sp-]のようにすればプリデクリメントになります。
I/O
データ空間の最初の8バイトを入力ポート、続く8バイトを出力ポートに割り当てます。
and/orでbit bangingできるように、出力ポートを読むと最後に出力した値が読める仕様にします。
ISA
アドレシング
000 adr ダイレクト(0〜511)
001 ofs[p] ポインタ+オフセット(-256〜+255)
010 ofs[p+] ポインタ+オフセット(-256〜+255),ポストインクリメント
011 ofs[p-] ポインタ+オフセット(-256〜+255),ポストデクリメント
100 #imm イミディエイト(0〜511)
イミディエイトは9ビットまでは命令中に埋め込み、
それを超えるとアセンブラがダイレクト領域に定数を埋め込み、ダイレクトアドレシングを生成する
演算・移動
00bb wooo DDDd dddd dddd SSSs ssss ssss
b:サイズ
w:書き込む
D:dstアドレシング
d:dstオフセット
S:srcアドレシング
s:srcオフセットまたはイミディエイト
o:演算種別
w=1 w=0
000 mov
001 and tst
010 xor
011 or
100 add
101 sub cmp
110 srl 両オペランドのアドレスは最上位を指定するが、その補正はアセンブラが行う
movを含め、全てフラグが変化する
複数バイトのとき:
8ビット単位で処理し、bit8=0とする
イミディエイトは最初のバイトだけ有効で、上位は0(符号拡張なし)
ポインタ
lsp/ldp address
1000 000t ---- ---- ---- ---s ssss ssss
ssp/sdp address
1001 000t ---- ---- ---- ---s ssss ssss
lsp/ldp #imm
1010 000t ---- ---- ---- -iii iiii iiii
ポインタのロード・ストアはダイレクトの範囲内に限る
t: 0のときsrc、1のときdst
分岐
1100 tttt ---- ---- ---- ---a aaaa aaaa
a:アドレス
t:種別
0xxx 無条件
1000 Z
1001 NZ
1010 Z9
1011 NZ9
1100 C
1101 NC
1110 C9
1111 NC9
コール・リターン
call 1111 ---- ---r rrrr rrrr ---a aaaa aaaa
ret 1110 ---- ---r rrrr rrrr ---- ---- ----
a:呼び出すアドレス
r:リターンアドレスをロード・ストアするアドレス
設計が終わったら
いろいろな作業が待っています。
ISAに基づいてアセンブラを作成...
エミュレータを作成...
テストプログラム(想定するアプリケーションの一部)を作成...
設計したISAが使いやすいかどうか検証して必要なら修正...
前掲のものは、その結果をまとめたものです。
これで行けそう、となったらVerilogで書き起こし、エミュレータと同じ動作になるまでデバッグします。
パイプライン方式も検討しました。
ただ、2ポート読み出しと1ポート書き込みを同時に行うので、データ容量の倍のBSRAMが必要になります。
オペランドが2つなので普通のデュアルポートで済みそうですが、読み出しと書き込みは別のステージなので3つ別々のアドレスになります。
メモリも含めてできるだけ小さくしたいので、パイプライン方式は採用しません。
ではせめてオペランド2つを同時に読み出そうと思ったら、Gowin BSRAM & SSRAM User Guideのp.6に
GW1NZ-1/GW1NZ-1C do not support dual port mode with 1/2/4/8/9 bit widths.
とあり、Tang Nano 1kで使えなくなります。
18ビット幅にして外部にセレクタなどを追加する手もありますが、回路も増えるので諦めました。
結果的に4クロック/命令が基本となりました。
演算・移動で複数バイトのときは1バイトごとに3クロック追加になります。
余談ですが、アセンブラをスクラッチから書くのは大変なので、以前作ったCコンパイラ
https://github.com/kwhr0/cckp/tree/master/cckp
を改造しました。
プリプロセッサや定数式の計算、エラー箇所の表示などはそのまま使えるので楽です。
Tang Nano 1kで作るVGMプレイヤー
VGMというのは、例えば
https://www.smspower.org/Competitions/Music
で公開されている、DCSG(SN76489A)へ送るデータをダンプしたファイルです。
# YM2413用は対象外です。
拡張子がVGMでも、中身はGZIPファイルなので、展開して、改めて拡張子VGMを付加します。
サンプルは、FAT16でフォーマットされたSDカードのルートディレクトリからVGMファイルを読み出し、演奏するアプリです。
小さなグラフィックディスプレイに曲番号、ファイル名、レベルメーターなどを表示します。
メーターは、縦方向に各チャネル、左側が周波数、右側がレベルです。
オシレータの音量はRGB LEDにも反映します。
Tang Nano基板上の2つのボタンで選曲、ベース基板上のボタンを押しながらだとボリューム操作になります。
VGMファイルはDCSGのクロック3.58MHz,fs=44.1kHzを想定しています。
ちょっと誤差はありますが、PLLで36MHzを作り、10分周(相当)しています。
fsはタイマーの待ち時間をその値で計算しますが、実際のfsは96kHz弱にしています。
DCSGのサウンド出力は16ビットですが、デジタルボリュームを介すのでI2Sは32ビットとしています。
I2C,I2S,SPI,タイマーなどはHDLで書いたので、組み込みマイコンっぽくなっています。
DCSG,ボリュームなども含め、リソースの使用状況は以下の通り。

CPUコアだけだと350Logic未満です。
表示については
https://github.com/tinusaur/ssd1306xled
を移植することで比較的簡単に出ましたが、SDカードはちょっと手こずりました。
実行するたびに異なる(0xf8とか0x80とか)、謎のレスポンスを返し続けるのです。
基板を作り始めてからは、この問題の解決に最も時間がかかりました。
結局、初期化時にCS=Hのままダミークロックを入れる処理が正しくないという、わかってしまえば単純なミスでした。
Gowin EDAでビルドするときは、初めに
https://github.com/sipeed/TangNano-1K-examples/blob/main/README.md
のようにしてSSPIとMSPIにチェックを入れます。
ところで、書いている途中で命令が512ワードを超えてしまったら使えなくなるのは困るので、
asm/main.cppとpico9.v両方のPCMSBを9にし、
//pc[PCMSB-9:0] <= 0;
のコメントを外すと1024ワードまで使えるようにしてあります。
この場合、リターンアドレスのLSBは保存されない(常に0とする)ので、アラインする必要がありますが、
これはアセンブラが自動的に行います。
そのため、プログラムが少し大きくなってしまいます。あくまで救済措置です。
アセンブラを書いていても(自分にとって)とても書きやすく、
これくらいの小さな用途に使いやすいプロセッサになったと思います。
https://github.com/kwhr0/pico9-cpu
以下の曲を演奏してみました。
https://www.smspower.org/Music/Troublemaker-Homebrew