Kanade Electronicskanaelecom
🏗

命令セットアーキテクチャ

ロジックIC で組む自作マイコン RK16 の機械語仕様

RK16 は 16bit データ幅/16bit アドレス幅、命令長 32bit のハーバードアーキテクチャ CPU です。命令メモリ (imem) と データメモリ (dmem) は完全に分離していて、レジスタファイルも dmem の先頭にメモリマップされています。

 この記事では機械語レベルの契約 — 命令フォーマット・opcode・ALU 演算・疑似命令 — を定義します。レジスタの ABI 上の役割は ABI を、メモリ空間全体は メモリマップ を、割り込み仕様は 割り込み処理 を、回路実装は マイコン回路 を参照してください。

命令フォーマット

 すべての命令は固定長 32bit で、上位 16bit に即値、下位 16bit に opcode とレジスタフィールドを並べます。

 31              16 15        12 11  8 7   4 3   0
+------------------+------------+-----+-----+-----+
|       imm        |  (alu/-)   |  rd | rs2 | rs1 | opc
+------------------+------------+-----+-----+-----+
       16bit          4bit       4bit  4bit  4bit
フィールドbit意味
opc[3:0]opcode (命令種別)
rs1[7:4]第1ソースレジスタ番号 (dmem アドレス下位 4bit)
rs2[11:8]第2ソースレジスタ番号
rd[15:12]デスティネーションレジスタ番号
alu/-[19:16]CALC では ALU 演算コード, 他は未使用
imm[31:16]16bit 即値
  • CALC 命令では imm フィールドの下位 4bit に ALU 演算コードが入ります。CALC は ALU の op がレジスタ間 (rs1, rs2) で確定するので即値を取らない代わりに、その領域に op コードを置きます。
  • CALCI 命令では rs2 フィールドに ALU 演算コードが入ります。レジスタ番号は 16 個 = 4bit に収まり、ALU op コードも 4bit なので、同じフィールドを兼用します。
  • LOAD / STORE / CTRL では ALU は ADD 固定です。実効アドレスは [rs1] + imm

 なぜ 32bit 命令長か。アドレス幅が 16bit なので、ジャンプ先や定数ロードを 1 命令で完結させるには即値も 16bit 必要です。RISC で典型的な 32bit 命令長は、この「即値 16bit + 制御 16bit」という分割と相性がよいのです。

opcode の 5 クラス

 機械語の命令種別は 5 つだけです。

Opopcode形式用途
CALC0b0000op rd, rs1, rs2レジスタ間 3 オペランド演算
CALCI0b0001op rd, rs, imm即値演算
LOAD0b0011op rd, rs, immメモリ読み出し
STORE0b0111op rs2, rs1, immメモリ書き込み
CTRL0b1111op rd, rs1, rs2, imm分岐 / 呼出 / 復帰

 opcode は全て「下位ビットから 1 が連続するパターン」になっていて、c[3]-c[0] を MUX のセレクタとして素直に使えます。バイナリ密度より、ロジック IC で組むときのデコーダの単純さを優先しています。具体的には次のような制御を想定しています。

制御信号立つ opcode意味
use_immCALCI, LOAD, STORE, CTRLALU の第 2 入力に即値を使う
mem_accessLOAD, STORE, CTRLデータメモリにアクセス (CTRL は読み)
mem_writeSTOREメモリ書き込み
branchCTRLPC 更新を ALU 結果から取る

ALU 演算 (14 種)

演算ニーモニック意味
0加算add[rd] = [rs1] + [rs2]
1減算sub[rd] = [rs1] - [rs2]
2ANDand[rd] = [rs1] & [rs2]
3ORor[rd] = [rs1] | [rs2]
4XORxornot(rd, rs)xori(rd, rs, 0xFFFF) で代用
5一致eq[rs1] == [rs2] なら 0xFFFF
6不一致neq[rs1] != [rs2] なら 0xFFFF
7大小 (符号無)lt[rs1] < [rs2] なら 0xFFFF
8大小 (符号付)lts符号付き比較
9右シフトsr[rd] = [rs] >> 1、上位は 0 埋め
A算術右シフトsrsMSB を保持 (符号拡張)
B右ローテートsrrMSB に LSB を巻き戻す
C左シフトsl[rd] = [rs] << 1
D左ローテートslrLSB に MSB を巻き戻す

 シフトは 1bit シフト固定で、N bit シフトしたい場合は同じ命令を N 回並べます。バレルシフタを持たないのは、ロジック IC でフルクロスバを組むと配線が爆発するためです。

比較演算の真偽値

 真偽は 16bit のままで 0x0000 を false、それ以外を true として扱います。比較結果は MSB から LSB まで全ビットに同じ値が広がるので、そのままビット演算で論理演算に流用できます。たとえば (a < b) && (c < d) は次のように書けます。

lt(t0, a, b);
lt(t1, c, d);
and(t2, t0, t1);   // t2 が 0xFFFF なら両方 true

符号拡張

 符号付き整数を上位に伸ばすとき、MSB を埋めるのが符号拡張です。srs は MSB を保持しながら右シフトするので、符号付き整数の >> 1 (整数除算) に使えます。

decbin
10000_0001
0000_0000 (sr で 0 埋め)
-11111_1111
1111_1111 (srs で 1 埋め)

CALC / CALCI

 CALC(op, rd, rs1, rs2)[rd] = [rs1] op [rs2] を実行します。op は ALU 演算コードで、imm[3:0] に符号化されます。

 CALCI(op, rd, rs, imm)[rd] = [rs] op imm を実行します。oprs2 フィールドに符号化され、imm フィールドは 16bit 即値そのものです。これにより、addi(t0, z, 0x1234) のような書き方で 16bit 定数の即値ロードが 1 命令で済みます。

LOAD / STORE

ASM意味
load(rd, rs1, imm)[rd] = mem[[rs1] + imm]
store(rs2, rs1, imm)mem[[rs1] + imm] = [rs2]

 実効アドレスは常に [rs1] + immstore には書き込み先レジスタが無いので、rd フィールドは未使用です。

 レジスタファイルが dmem の先頭にあるおかげで、メモリと同じインタフェースでレジスタ操作ができます。たとえば MMIO の serial_rx (0x1001) を直接読み出すには次のように書きます。

load(t0, z, 0x1001);   // z = 0 なので実効アドレスは 0x1001

CTRL

 CTRL は分岐・関数呼出・復帰を扱う唯一の命令クラスです。フォーマットは CTRL(rd, rs1, rs2, imm) で、次のような意味論を持ちます。

  • 目的アドレス: [rs1] + imm を ALU で計算
  • 条件: [rs2] != 0 の時に分岐 (rs2=z なら無条件)
  • 副作用: [rd] = PC + 1 (呼出時はリターンアドレスを書き込む。rd=z なら破棄)

 この 1 命令で jump / jumpr / jumpif / call / ret / iret を表現します。

疑似命令

 アセンブラ (tasm) からは、CALC / CALCI / CTRL の特定パターンに名前を付けた疑似命令を書きます。movloadi は機械語上は add / addi ですが、可読性のために別ニーモニックを与えています。

疑似命令機械語意味
nop()CALC(ADD, z, z, z)何もしない
mov(rd, rs)CALC(ADD, rd, rs, z)[rd] = [rs]
loadi(rd, imm)CALCI(ADD, rd, z, imm)[rd] = imm
not(rd, rs)CALCI(XOR, rd, rs, 0xFFFF)[rd] = ~[rs]
jump(imm)CTRL(z, z, z, imm)PC = imm (無条件)
jumpr(imm)CTRL(z, pc, z, imm)PC = PC + imm (相対)
jumpif(rs, imm)CTRL(z, z, rs, imm)if [rs] != 0PC = imm
jumpifr(rs, imm)CTRL(z, pc, rs, imm)if [rs] != 0PC = PC + imm
call(imm)CTRL(ra, z, z, imm)ra = PC+1; PC = imm
callr(rs)CTRL(ra, rs, z, 0)レジスタ経由の関数呼出
ret()CTRL(z, ra, z, 0)PC = ra
iret()CTRL(z, ira, z, 0)PC = ira (割込復帰)

 CTRL のフォーマットからすべての分岐・呼出・復帰が機械的に派生していることが分かります。デコーダもこの規則性に乗っかって書けるので、回路の重複を抑えられます。

コード例

即値ロードと算術

loadi(t0, 0x1234);     // t0 = 0x1234
loadi(t1, 0x5678);     // t1 = 0x5678
add(t2, t0, t1);       // t2 = 0x68AC

条件分岐 (if)

// if (a == b) goto label;
eq(t0, a, b);
jumpif(t0, label);

ループ (while)

loop:
    lt(t0, i, n);          // t0 = (i < n) ? 0xFFFF : 0
    eqi(t1, t0, 0);        // t1 = !t0
    jumpif(t1, loop_end);  // 条件不成立で抜ける
    // ... 本体 ...
    addi(i, i, 1);
    jump(loop);
loop_end:

複合条件 (a<b) && (c<d)

lt(t0, a, b);
lt(t1, c, d);
and(t2, t0, t1);
jumpif(t2, both_true);

MMIO への書き込み (1 文字送信)

loadi(t0, 'A');
store(t0, z, 0x1000);  // mem[0x1000] = 'A'  -> シリアル送信

関連項目