8ビット独自命令セット自作CPUを作ってみた


事始め

去年のアドベントカレンダーでTD4を実装した記事を書きましたが、TD4は実用性がなく、もっと実際に使えるようなCPUを作りたくなりました。
ということで始めた二番目のCPU自作です。今回のVerilogで書くことにしました。
8ビットのマイコンとして使えるCPUを目指しました。

命名

初めての独自命令セットのCPUということで「歩む」、また虹ヶ咲スクールアイドル同好会のアニメのキャラクターの登場順序によって一番最初の「上原歩夢」から「歩夢」。
名前は「AYUMU」になりました。

スペック

16ビットの固定命令長の命令セットです。プログラムのメモリアドレスは16ビット(2バイト)を単位としてつけられます。メモリアドレスは最大16ビットで、最大65536個の命令を持つプログラムを作成することができます。

8ビットの加減乗算及び論理演算(AND, OR, XOR, NOT)をサポートし、条件分岐もサポートしています。また、割り込み処理のための専用命令も用意しました。

プログラムはROMに直接書き込みます。今回はVerilogで直接機械語の命令を書き込みました。
データメモリは8ビット単位で8ビットアドレスであり、合計256バイトのデータメモリを持っています。

レジスタ

AYUMUには汎用レジスタ、入出力レジスタ、割込レジスタが存在します。
R0~R15の合計16本の汎用レジスタ、Out0~Out3・In0~In3のそれぞれ4本ずつの入力及び出力レジスタ、T0~T3のタイマー割込レジスタ、INT0~INT3の入力割込レジスタが存在します。

汎用レジスタ及び入出力レジスタは8ビットのレジスタです。8ビットのマイコンのように扱えばいいと思います。

T0~T3及びINT0~INT3の割込レジスタはポインタとフラグで分かれています。
ポインタレジスタには16ビットの割込発生時に飛ぶメモリアドレスを格納します。
フラグレジスタには8ビットのフラグを格納します。フラグの機能は下に示しています。

ビット 機能
0 割込発生フラグ(0: X, 1: O)
1 入力割込発生条件(0: 0→1, 1: 1→0)
2~7 タイマー倍率

命令セット

レジスタ+即値タイプ

OPCODE(4bit) + REG(4bit) + CONSTANT(8bit)
名前 ビット列 演算/機能
addi 1000 reg = reg + constant
subi 1001 reg = reg - constant
muli 1010 reg = reg * constant
shli 1011 reg = reg << constant
shri 1100 reg = reg >> constant
andi 1101 reg = reg & constant
ori 1110 reg = reg | constant
xori 1111 reg = reg ^ constant

レジスタ+レジスタタイプ

OPCODE(8bit) + REG1(4bit) + REG2(4bit)

汎用レジスタ= 汎用レジスタ+汎用レジスタ

名前 ビット列 演算/機能 備考
add 01000000 reg1 = reg1 + reg2
sub 01000100 reg1= reg1 - reg2
mul 01001000 reg1 = reg1 * reg2 符号付き整数
shl 01001100 reg1 = reg1 << reg2
shr 01010000 reg1 = reg1 >> reg2
and 01010100 reg1 = reg1 & reg2
or 01011000 reg1 = reg1 | reg2
xor 01011000 reg1 = reg1 ^ reg2
mulu 01011100 reg1 = reg1 * reg2 符号なし整数
addc 01100000 reg1 = reg1 + reg2 + C
subc 01101000 reg1 = reg1 - reg2 + C

分岐フラグ = 汎用レジスタ+汎用レジスタ

名前 ビット列 演算/機能 備考
beg 01000001 F = reg1 == reg2
bne 01000101 F = reg1 != reg2
blt 01001001 F = reg1 < reg2 符号なし整数
bgt 01001101 F = reg1 > reg2 同上
ble 01010001 F = reg1 <= reg2 同上
bge 01010101 F = reg1 >= reg2 同上

入出力/割込レジスタの値の設定(割込レジスタ+汎用レジスタ)

intputreg, outputreg, interruptregはそれぞれ入力レジスタ、出力レジスタ、割込レジスタを意味します。
[pointer]は割込発生時のジャンプ先、[flag]は割込発生フラグを意味します。
inputreg, outpureg, interruptregをreg1、regをreg2と考えてください。

名前 ビット列 演算/機能
rd 01000010 reg <- inputreg
wt 01000110 outputreg <- reg
intp 01010010 interruptreg[pointer] <- reg
intf 01100010 interruptreg[flag] <- reg

レジスタ組み合わせ番号タイプ

OPCODE(12bit) + NO(4bit)

NOは0001(R12+R13)と0010(R14+R15)が存在します。このNOは16ビットのメモリアドレスを8ビットのレジスタで表すための手段です。

名前 ビット列 演算/機能
jmp 000100000000 NOへジャンプ
jmpf 000100010000 分岐フラグが1の場合NOへジャンプ

ロード・ストア命令

データメモリへのロード・ストアのための命令です。

OPCODE(8bit) + REG1(4bit) + REG2(4bit)

REG1はロード先(ロード時)またはストア元(ストア時)でREG2はメモリのアドレスを示します。

名前 ビット列 演算/機能
st 01000011 mem[reg2] <- reg1
ld 01000111 reg1 <- mem[reg2]

レジスタ一個タイプ

一つしかありません。

名前 ビット列 演算/機能
not 000100000001 reg = ~reg

その他

名前 ビット列 演算/機能
jmpp 0001000000100000 割込ハンドラから抜ける

テストプログラム

タイマー割込を用いて一定時刻ごとにカウントアップし、出力するプログラムです。

xor r0, r0
addi r0, 201
intf T0, r0
xor r8, r8
xor r12, r12
xor r13, r13
addi r12, 0
addi r13, 15
intp T0, 0 #NOの0だからr12,r13の値(15)
wt Out0, r1
xor r12, r12
xor r12, r13
addi r12, 0
addi r13, 9
jmp 0 #NOの0だからr12,r13の値(9)
st r8, r8
addi r8, 1
st r12, r8
addi r8, 1
st r13, r8
addi r1, 1
wt Out0, r1
ld r13, r8
subi r8, 1
ld r12, r8
subi r8, 1
ld r8, r8
jmpp

動作確認


点灯しているのが確認できます。一定時刻が経つと点灯するLEDが変化します。数は二進数で表現されます。

コード

Githubのレポジトリに公開しています。今でもいくつかのミスは見つけましたが、保守のための余裕がないためコードの修正とかはできていません…プルリクエスト等の受理できないので予めご了承ください。

最後に

初めて自分で命令セットを設計し、それを実装したことに意義があると思っています。実装が汚い、無駄な実装がある、命令セットの設計が良くないなど、いろんな問題点のある作品ですが、どうか大目に見てください。
次はRISC-V命令セットの32ビットCPUを設計しようと思っています。上手くいくかどうかわかりませんが…