自作マイコンのアーキテクチャ RK16

2023-03-26

ロジックICを使った自作マイコンのアーキテクチャです。

\[ \newcommand{dn}[3]{\frac{\mathrm{d}^{#3} #1}{\mathrm{d} #2^{#3}}} \newcommand{\d}[2]{\frac{\mathrm{d} #1}{\mathrm{d} #2}} \newcommand{\dd}[2]{\frac{\mathrm{d}^2 #1}{\mathrm{d} {#2}^2}} \newcommand{\ddd}[2]{\frac{\mathrm{d}^3 #1}{\mathrm{d} {#2}^3}} \newcommand{\pdn}[3]{\frac{\partial^{#3} #1}{\partial {#2}^{#3}}} \newcommand{\pd}[2]{\frac{\partial #1}{\partial #2}} \newcommand{\pdd}[2]{\frac{\partial^2 #1}{\partial {#2}^2}} \newcommand{\pddd}[2]{\frac{\partial^3 #1}{\partial {#2}^3}} \newcommand{\p}{\partial} \newcommand{\D}[2]{\frac{\mathrm{D} #1}{\mathrm{D} #2}} \newcommand{\Re}{\mathrm{Re}} \newcommand{\Im}{\mathrm{Im}} \newcommand{\bra}[1]{\left\langle #1 \right|} \newcommand{\ket}[1]{\left|#1 \right\rangle} \newcommand{\braket}[2]{\left\langle #1 \middle|#2 \right\rangle} \newcommand{\inner}[2]{\left\langle #1 ,#2 \right\rangle} \newcommand{\l}{\left} \newcommand{\m}{\middle} \newcommand{\r}{\right} \newcommand{\f}[2]{\frac{#1}{#2}} \newcommand{\eps}{\varepsilon} \newcommand{\ra}{\rightarrow} \newcommand{\F}{\mathcal{F}} \newcommand{\L}{\mathcal{L}} \newcommand{\t}{\quad} \newcommand{\intinf}{\int_{-\infty}^{+\infty}} \newcommand{\R}{\mathcal{R}} \newcommand{\C}{\mathcal{C}} \newcommand{\Z}{\mathcal{Z}} \newcommand{\bm}[1]{\boldsymbol{#1}} \]

レジスタ

メモリ空間の上位16個(0x000x)をレジスタとして割り当てます。

アドレス 機能
特殊レジスタ
0x0000 zero ゼロ固定 R
0x0001 ira 割り込み戻りアドレス R
0x0002 pc プログラムカウンタ R
0x0003 sp スタックポインタ R&W
汎用レジスタ
0x0004 ra 戻りアドレス
0x0005 fp フレーム・ポインタ
0x0006 - 0x0007 a0 - a1 引数戻値レジスタ
0x0008 - 0x000b t0 - t3 一時レジスタ
0x000c - 0x000f s0 - s3 保存レジスタ

 ロジックICでCPUを作る場合、レジスタは74HC564などのDフリップフロップを使用します。しかし、今回作るCPUはレジスタが多く(16個)、ロジックICの数が膨大(74HC564×32個)になってしまいます。そのため、このCPUでは、メインメモリのSRAMの一部を、レジスタとして使います。

 レジスタをSRAMとして実装する問題点として、レジスタの値をリアルタイムに取得できないという問題があります。多くの自作CPUは、レジスタの値を直接LEDで見れるようになっています。

 この問題を解消するため、メモリバースト転送という仕組みを実装します。各クロックの最後のステージで、デバッガがRAMの内容を全て読み出します。レジスタ値の変化をリアルタイムに見ることはできませんが、命令によるレジスタ値の変化を追跡することができます。

特殊レジスタ

 レジスタをSRAMとして実装すると述べましたが、SRAMの入出力ポートは1つしかないので、複数のレジスタ値を常に取得し続けることができないという問題があります。そのため、4個のレジスタはSRAMとは独立したDフリップフロップICで実装します。アドレスの上位12ビットが0の場合 0b0000_0000_0000_00xx SRAMのCE信号をOFFにして、DFFのCE信号をONにします。(CE:Chip Enable)

0. ゼロ・レジスタ(zero)

 常にゼロを返すレジスタです。

1. 割り込み戻りアドレス(ira)

 割り込みが発生したら、次のクロックでPCを割り込みのアドレス(0x0001)に変更し、割り込みの処理を行います。その時に、本来次に行くべきPCを退避しておくレジスタが、割り込みリターンアドレス (IRA : Interrupt Return Address) です。

2. プログラムカウンタ(pc)

 現在実行中のプログラムのアドレスを指します。ただ、リードオンリーで通常のレジスタアクセスでは書き換えられません。PCを書き換えるには、制御命令を使う必要があります。

3. スタック・ポインタ(sp)

 スタックの先頭を指したポインタです。push/pop命令でカウントアップ/カウントダウンされます。

汎用レジスタ

 0x0004-0x000fは、汎用レジスタに割り当てられています。汎用レジスタは、それぞれに特殊な機能があるわけではなく、ハードウェア的にはどれも同じなのですが、標準的な使い方を ABI (Application Binary Interface) で規定しています。詳細は関数呼び出しの項で。

0. 戻りアドレス(ra)

 関数の戻りアドレスを退避しておくレジスタです。

1. フレームポインタ(fp)

 関数フレームの基底アドレスを指したポインタです。

2. 引数戻値レジスタ(a0-a1)

 関数の引数を入れておくレジスタです。

3. 一時レジスタ(t0-t3)

 関数呼び出しの後で値が変化するレジスタです。

4. 保存レジスタ(s0-s3)

 関数側が関数の前後で値が変化しないことを保証するレジスタです。

メモリ

 メモリマップドIOです。

アドレス 機能
0x0000 - 0x000f レジスタ
0x0010 - 0x00ff 状態制御レジスタ
0x0100 - 0x0fff 入出力制御レジスタ
0x1000 - 0x2fff VRAM
0x3000 - 0x4000 EEPROM
0x5000 - 0xffff RAM

状態制御レジスタ

 CSR (Control & Status Registors)は、割り込み・例外・特権レベルなど、プロセッサの状態を保持するレジスタです。

アドレス R/W 機能
W 割り込み許可
W 割り込みマスク
R 割り込み番号
内部例外
※特権モード

入出力制御レジスタ

 入出力制御レジスタは、シリアル通信、PWM、ADCなど、入出力モジュールを制御するレジスタです。

VRAM

 VRAMは、ディスプレイ表示用のRAMです。同時に2つのポートから読み書きができる Dual Port RAM を使って実装します。表示の方法として 2 種類のモードを切り替えることを考えています。

ピクセル描画モード

 ひとつはピクセル描画モードです。VRAM の bit と 300 x 400 ピクセルを一対一に対応させ白黒で描画します。必要な VRAM のサイズは 300 x 400 x 1 bit = 120000 bit = 16 bit x 7500 < 16bit x 0x2000

テキスト描画モード

 ふたつめはテキスト描画モードです。VRAM を 1 word ごと文字にして表示します。フォントを別の EEPROM に置いておき、描画時に組み合わせます。

EEPROM

 メモリ空間中にEEPROMがあります。これは、定数値を置いておくための領域です。

RAM

 メインの RAM です。SRAMで作ります。

ROM

 ハーバードアーキテクチャなので、データを保持するRAM空間と別に、プログラムを保持するROM空間があります。ROM は高速な NOR Flash で作ります。

命令セット

 機械語的に見ると命令は calc,calci,load,store,calif の5種類です。しかし、アセンブラから見ると、??種類の命令があります。多くのアセンブラでは「疑似命令」と呼ばれるシンタックスシュガーを実装しています。例えば、RISC-Vでは、add rd, rs, zeromov rd, rs と短縮して書くことができます。しかし、RKASMの疑似命令はシンタックスシュガーではありません。その特徴が一番現れているのが calif 命令です。califをニーモニックとして使うことはできません。アセンブラ if sa2 sa1 imm は、内部的には calif zero sa2 sa1 imm と等価ですが、アセンブラ上で直接 calif zero sa2 sa1 imm と書くことはできません。別にこれを書けるようにしてもいいんですが、引数を4つも取ると、アセンブラが見にくくなるという問題があります。また実用上、引数4つをフルで使うことはない(どれかがzeroになる)ので、califを直接呼び出せないようにしています。その代わりに、使い方に応じてニーモニックif jump call ret iret を割り当てています。これによって「生で書いてもわかりやすい&バグりにくいアセンブラ」を実現します。

 RK16の命令は「RAMから2回読み出し1回書き込む」という形式をとっています。これにより、全ての命令が同じステージ数で実行できるようになり、命令デコーダ (ID:Instruction Decoder) の回路を簡略化できます。

 機械語命令は5種類あるので、命令ビットは 3 ビットで済むはずです。また、バイナリ中で最も多く出現する mov 命令は、意味のあるビットは 12/32 と、命令空間をかなり無駄遣いしています。つまり、この命令セットは情報量に対してかなり余剰なROM空間を消費するということになります。一般的に、特にROM空間が限られる組み込みにおいて、命令セットはバイナリ長が短くなるものが良いです。しかし、RKISAはロジックICで実装するために、ハードウェアの削減に重きをおいています。命令デコーダ(ID)の実装が楽になるように、あえて余分なビットを使っています。

演算命令

命令 ASM 処理
レジスタ演算 add da sa1 sa2 [da] = [sa1] + [sa2]
即値演算 addi da sa1 imm [da] = [sa1] + imm

算術演算

演算 ASM
+ 加算 add
- 減算 sub
& ビット AND and
| ビット OR or
^ ビット XOR xor
~ ビット NOT not
>> 右シフト(符号あり) rss シフトして符号拡張
>> 右シフト(符号なし) rsu 0 埋め
<< 左シフト ls

符号拡張 (Sign Extension) とは

 4bitの符号付き整数を8bitにしたい場合、正の数は0を埋めればいいですが、負の数は1を埋める必要になります。符号付き整数の符号は、最上位ビットで表されるので、一般に、符号付き整数のビット長を伸ばしたい場合、最上位ビットで埋める必要があります。これが符号拡張です。

dec bin
1 0000 0001
0001
0 0000
1111
-1 1111 1111

比較演算

 真偽型は、0x0000 のみを false とし、他は全て true とみなします。

演算 ASM
== 一致比較 eq
!= 不一致比較 neq
< 大小比較(符号あり) lts signed として比較
< 大小比較(符号なし) ltu unsigned として比較

 比較演算は true = 0xFFFF false = 0x0000 を返します。論理キャストは false(0x0000) 以外の値を 0xFFFF に変換します。論理演算をビット演算で行えます。 真偽値型へのキャストは neq x zero でできます。

転送命令

命令 ASM
レジスタ間移動 mov da sa [da] = [sa]
即値ロード loadi da imm [da] = imm
メモリロード load da sa1 imm [da] = [[sa1] + imm]
メモリストア store sa2 sa1 imm [[sa1] + imm] = [sa2]

 ロード命令は、メモリからレジスタに値を移動します。ストア命令は、レジスタからメモリに値を移動します。メモリのアドレスは、レジスタに入ってる値と即値を足します。(mem[sa1 + imm])

制御命令

命令 ASM 処理
条件呼出 [da] = PC + 1, if([sa2] == 0) PC = [sa1] + imm
条件分岐 if sa2 imm if([sa1] == 0) PC = imm
ジャンプ jump imm PC = imm
関数呼出 call imm [RA] = PC + 1, PC = imm
関数復帰 ret PC = [RA]
割込復帰 iret PC = [IRA]

 プログラムの制御に関する命令は全て calif で実行できます。

回路

命令の実行

 RK16の命令は「RAMから2回読み出し1回書き込む」という形式をとっています。これを4ステージで実行します。

ステージ calc calci load store calif
1.Load [sa1] => SR1
[sa1] => SR1
ALU(SR1,imm,func)
[sa1] => SR1
ALU(SR1,imm,add)
[sa1] => SR1
ALU(SR1,imm,add)
[sa1] => SR1
ALU(SR1,imm,add)
2.Load [sa2] => SR2
ALU(SR1,SR2,func)
[ALU] => SR2
[sa2] => SR2
[sa2] => SR2
IS_ZERO(SR2)
3.Store ALU => [da] ALU => [da] SR2 => [da]
(SP+=sd)
SR2 => [ALU/SR1]
(SP-=sd)
PC+1 => [da]
4.Dump PC+1 => PC PC+1 => PC PC+1 => PC PC+1 => PC IS_ZERO ? ALU : PC+1 => PC

 命令に合わせて、各ステージの長さを調整することで、クロック(正確にはIPS)を上げられます。

スタック

 RK16 では、スタックの pop / push とメモリの load / store を同じ命令で実行できます。正確にいえば、load/store命令のオプションとして、スタックポインタのインクリメント/デクリメントを行うことができます。RK16のビットフィールドを見ると、loadでは[11:8]が、storeでは[15:12]がオプションのフィールドです。また、load/storeのアドレスが計算された後にSPが変更されるため、pop命令では即値にオフセット1を指定する必要があります。

 また、余談ですが、標準的にはアドレスが減る方向に伸びますが、逆方向にスタックを伸ばすこともできるようにはなっています。コンパイラとかは対応していませんが暇があったら実装しようかな。スタックをアドレス増加方向に伸ばすと、スタックが予想外に伸びたときに、たいていはアドレスが小さい領域にある重要なデータを破壊せずに、セグメンテーション違反で止まってくれるとかいういいことがあるらしい。

割り込み

1. 割り込み発生

割り込みは、割り込み入力ピン i0-i71 にすることで発火できます。割り込み発生時に実行されている命令が終了したら、割り込み処理へ遷移します。

2. 割り込み開始

実行中の命令が終了したら、割り込み処理へ移動します。

  1. 次のPCをIRAに退避します。
  2. PCを割り込みのアドレス(0x0001)にします。

3. 割り込み禁止

割り込み処理中に割り込みが発生しないように、割り込み許可ビットを0にします。

4. 割り込み処理の本体

割り込みの処理を行って、割り込みを解消します。

5. 割り込み許可

割り込み処理から復帰する前に、割り込み許可ビットを1に戻します。変更が反映されるまで、ハードウェア的に1クロック遅延させています。割り込みから復帰する前に割り込みが発生すると、IRAが上書きされ、元の処理がわからなくなります。

6. 割り込みから復帰

iret 命令で割り込みから元の処理に戻ります。

例外