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 つだけです。
| Op | opcode | 形式 | 用途 |
|---|---|---|---|
| CALC | 0b0000 | op rd, rs1, rs2 | レジスタ間 3 オペランド演算 |
| CALCI | 0b0001 | op rd, rs, imm | 即値演算 |
| LOAD | 0b0011 | op rd, rs, imm | メモリ読み出し |
| STORE | 0b0111 | op rs2, rs1, imm | メモリ書き込み |
| CTRL | 0b1111 | op rd, rs1, rs2, imm | 分岐 / 呼出 / 復帰 |
opcode は全て「下位ビットから 1 が連続するパターン」になっていて、c[3]-c[0] を MUX のセレクタとして素直に使えます。バイナリ密度より、ロジック IC で組むときのデコーダの単純さを優先しています。具体的には次のような制御を想定しています。
| 制御信号 | 立つ opcode | 意味 |
|---|---|---|
use_imm | CALCI, LOAD, STORE, CTRL | ALU の第 2 入力に即値を使う |
mem_access | LOAD, STORE, CTRL | データメモリにアクセス (CTRL は読み) |
mem_write | STORE | メモリ書き込み |
branch | CTRL | PC 更新を ALU 結果から取る |
ALU 演算 (14 種)
| 演算 | ニーモニック | 意味 | |
|---|---|---|---|
| 0 | 加算 | add | [rd] = [rs1] + [rs2] |
| 1 | 減算 | sub | [rd] = [rs1] - [rs2] |
| 2 | AND | and | [rd] = [rs1] & [rs2] |
| 3 | OR | or | [rd] = [rs1] | [rs2] |
| 4 | XOR | xor | not(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 | 算術右シフト | srs | MSB を保持 (符号拡張) |
| B | 右ローテート | srr | MSB に LSB を巻き戻す |
| C | 左シフト | sl | [rd] = [rs] << 1 |
| D | 左ローテート | slr | LSB に 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 (整数除算) に使えます。
| dec | bin |
|---|---|
| 1 | 0000_0001 |
0000_0000 (sr で 0 埋め) | |
| -1 | 1111_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 を実行します。op は rs2 フィールドに符号化され、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] + imm。store には書き込み先レジスタが無いので、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 の特定パターンに名前を付けた疑似命令を書きます。mov や loadi は機械語上は 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] != 0 で PC = imm |
jumpifr(rs, imm) | CTRL(z, pc, rs, imm) | if [rs] != 0 で PC = 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' -> シリアル送信