事始め
去年のアドベントカレンダーで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を設計しようと思っています。上手くいくかどうかわかりませんが…