割り込み (interrupt) は、外部要因によって CPU を「いま実行している命令とは別の処理」へ強制的に飛ばす仕組みです。タイマー・シリアル・ボタン入力 — どれも「いつ来るか分からない」イベントを、ポーリングではなくハードウェアの自動制御で拾うために必要になります。
RK16 では入力線 intr0-intr3 の 4 本を持ち、固定の割り込みベクタ 0x0004 にジャンプします。この記事ではハードウェアの挙動・例外モデル・コンテキスト退避・ハンドラ実装まで、上から順に見ていきます。
概要
割り込みは「外部要因による例外」の一種です。RK16 では:
- 割り込み入力 4 本 (
intr0-intr3) - 受理時にハードウェアが PC を
iraに退避し、PC を0x0004に飛ばす csr.ENABLE(bit 0) で全体許可、csr.MASK0-MASK3(bit 2-5) で個別マスクcsr.bit8-bit11に発火フラグ- 復帰は
iret()(CTRL 命令、PC = ira)
ハードウェアの挙動
割り込みは「命令の境界」でしか受理されません。実行中の命令を中断したりはしません。intr_i (i = 0..3) のいずれかが立っており、かつ csr.ENABLE = 1 かつ対応する csr.MASKi = 0 であれば、現在の命令の Dump ステージ後に次のシーケンスが走ります。
- 現在の PC (= 次に実行する命令のアドレス) を
iraに退避 csr.ENABLE(bit 0) を 0 に落とす (多重割り込みの防止)csr.bit (8+i)に発火フラグを立てる- PC を割り込みベクタ
0x0004に設定
これは全てハードウェアが命令と命令の隙間で行う制御であり、ソフトウェアは何もしません。割り込みが受理された瞬間、次に実行されるのはベクタ 0x0004 の命令です。
割り込みベクタ
ベクタ 0x0004 はリセットベクタ (0x0000) の直後に置かれます。tasm では次のように配置します。
asm @ 0x0000 reset {
loadi(fp, 0xffff);
call(init);
halt:
jumpr(halt);
}
asm @ 0x0004 irq {
jump(_intr); // 退避処理付きのプロローグへ
}
ベクタは 1 命令分しか領域がないので、実体のプロローグ (_intr) は別の場所に置き、ベクタからジャンプします。
CSR レジスタの詳細
| bit | 名前 | 役割 |
|---|---|---|
bit 0 | ENABLE | 1 で割り込み全体許可、0 で完全禁止 |
bit 1 | (予約) | |
bit 2 | MASK0 | 1 で intr0 を個別マスク |
bit 3 | MASK1 | 1 で intr1 を個別マスク |
bit 4 | MASK2 | 1 で intr2 を個別マスク |
bit 5 | MASK3 | 1 で intr3 を個別マスク |
bit 6-7 | (予約) | |
bit 8 | FIRE0 | intr0 の発火フラグ (ハードウェアがセット、ソフトがクリア) |
bit 9 | FIRE1 | intr1 発火フラグ |
bit 10 | FIRE2 | intr2 発火フラグ |
bit 11 | FIRE3 | intr3 発火フラグ |
CSR はレジスタなので、
ori(csr, csr, 0x0001); // ENABLE 許可
andi(csr, csr, 0xFFFE); // ENABLE 禁止
andi(csr, csr, 0xFEFF); // FIRE0 クリア (bit 8)
のように 1 命令で制御できます。
マスクと発火フラグの違い
- マスク (MASKi): 受理判定の段階で使う。マスクされていると受理自体が起こらない (発火フラグも立たない)
- 発火フラグ (FIREi): 受理時にハードウェアが立てる。ハンドラがクリアしないと、ENABLE 再許可後に同じ割り込みが再度受理されたように見える (実際には立ったまま)
多重割り込みの防止
ハードウェアは受理と同時に ENABLE = 0 に落とします。ハンドラ内では割り込みが入らないので、コンテキスト退避途中で破壊される心配がありません。
ハンドラ末尾の典型パターン:
ori(csr, csr, 0x0001); // ENABLE 再許可
iret(); // PC = ira
順序が重要です。iret の前に ENABLE を再許可しないと、その後の割り込みが永久に止まります。逆に ENABLE を許可してから iret までの間に別の割り込みが入ると、その瞬間は ira が上書きされる可能性があります — ハードウェアは「次の命令で iret する」とは知らないので。
厳密に二重割り込みを防ぐには、退避が完全に終わるまで ENABLE = 0 のままにし、ハンドラ本体に入る直前で再許可する設計もあります。RK16 RTOS では協調型なのでハンドラ全体を ENABLE = 0 のまま走らせ、最後に再許可してから iret します。
例外モデル
「例外 (exception)」は、通常の命令フローを離脱させる事象全体を指す傘の用語です。RK16 で実装しているのは割り込みのみですが、将来拡張の枠として次の分類を持っています。
- 外部要因 (命令と非同期に発生)
- 割り込み (Interrupt): 外部デバイスからの通知。RK16 では
intr0-intr3の 4 本
- 割り込み (Interrupt): 外部デバイスからの通知。RK16 では
- 内部要因 (実行中の命令の結果として発生)
- トラップ (Trap): システムコールなど、ソフトウェアが意図的に発生させる遷移。
syscall命令など - フォールト (Fault): 回復可能。命令を再実行できる。ページフォルトなど
- アボート (Abort): 回復不能。例: メモリパリティエラー
- トラップ (Trap): システムコールなど、ソフトウェアが意図的に発生させる遷移。
RK16 のハードウェアは現状トラップ命令を持たないので、システムコールに相当するものはユーザコードからの通常の関数呼出として実装します (Ring 0/Ring 3 分離もありません)。
コンテキスト退避
ベクタからジャンプしてくる _intr プロローグは、まず全レジスタをスタックに退避します。退避領域は callee 規約の延長で、FP から下向きに 12 word 取ります。
| FP オフセット | 内容 | 備考 |
|---|---|---|
fp + 0 | ra | 退避時の戻りアドレス |
fp + 1 | 旧 fp | 中断時の FP (再開後の FP に戻す) |
fp + 2 | t0 | |
fp + 3 | t1 | |
| … | … | |
fp + 11 | t9 |
pc, ira, csr は退避領域には書きません — pc はハードウェアが ira に保存済み、csr は全体で 1 つしかないため (タスクごとに切り替えるなら別途 TCB に保存)。
_intr の実コード (RTOS の中身):
asm _intr {
subi(fp, fp, 12); // 退避領域を確保
store(ra, fp, 0);
store(t0, fp, 2);
store(t1, fp, 3);
store(t2, fp, 4);
store(t3, fp, 5);
store(t4, fp, 6);
store(t5, fp, 7);
store(t6, fp, 8);
store(t7, fp, 9);
store(t8, fp, 10);
store(t9, fp, 11);
addi(t0, fp, 12); // 中断時の FP を計算
store(t0, fp, 1); // 旧 fp スロットに書く
// 中断時の ira / fp を TCB に保存
load(t0, z, task_running@);
store(ira, t0, (@(0 as *TCB)).ra@);
store(fp, t0, (@(0 as *TCB)).fp@);
jump(intr_dispatch);
}
なぜ ira と fp を TCB に保存するかというと、ハンドラ実行中に別タスクへ切り替える可能性があるからです。スケジューラが現在の TCB の中断時の状態を読んで、後で復帰できるようにしておきます。
割り込みディスパッチャ
退避が終わると intr_dispatch() に飛びます。これは普通の tasm 関数で、CSR の発火フラグを順に見てユーザハンドラを呼びます。
fn intr_dispatch() {
if ((csr & 0x0100) != 0) { intr0(); } // FIRE0
if ((csr & 0x0200) != 0) { intr1(); } // FIRE1
if ((csr & 0x0400) != 0) { intr2(); } // FIRE2
if ((csr & 0x0800) != 0) { intr3(); } // FIRE3
os_task_scheduler(); // 戻らない
}
ディスパッチ後はスケジューラに飛び、ready なタスクへコンテキストを切り替えて iret します。割り込みハンドラ自体は普通の関数として書けるので、ABI 規約 (引数なし / 戻り値なし) に従えば OK。
ユーザハンドラの書き方
ユーザは intr0() から intr3() の関数を定義するだけで、ハードウェアからの割り込みを処理できます。最低限の責任は 2 つだけ。
- 実際の処理を行う
- 自分の発火フラグを クリアする
fn intr0() {
// 例: タイマー割り込みでカウンタを進める
timer_count = timer_count + 1;
// 発火フラグ FIRE0 (bit 8) をクリア
csr = csr & 0xFEFF;
}
クリアを忘れると、os_task_scheduler から iret で復帰した瞬間に同じ割り込みが再受理され、無限ループになります。逆にクリアを最後に置くことで、「ハンドラ実行中に同じデバイスから次のイベントが来ても、フラグがまだ立っているので取りこぼさない」という挙動になります。
時分割スケジューリングの例
ハードウェアタイマからの周期的な intr0 で task_list を 1 つだけ回転させると、ready タスクが順番に選ばれる簡易ラウンドロビンになります。
fn intr0() {
// [head, A, B, tail] -> [A, B, tail, head]
if (task_list != (0 as *TCB)) {
var head: *TCB = task_list;
var second: *TCB = (@head).next as *TCB;
if (second != (0 as *TCB)) {
var tail: *TCB = head;
while ((@tail).next != (0 as *void)) {
tail = (@tail).next as *TCB;
}
(@tail).next = head as *void;
(@head).next = 0 as *void;
task_list = second;
}
}
csr = csr & 0xfeff; // FIRE0 クリア
}
この 1 個のハンドラだけで、task_list の先頭が毎回ずれていきます。ディスパッチャの後にスケジューラが呼ばれて、新しい先頭の ready タスクが走ります。
iret の動作
iret() は機械語上 CTRL(z, ira, z, 0) で、つまり PC = [ira] + 0 を実行する CTRL 命令の特殊形です。rd = z なので戻りアドレス書き込みは破棄、rs2 = z なので無条件分岐。
iret 自体は csr.ENABLE を触りません。ENABLE の再許可はソフトウェア責任なので、ハンドラ末尾で:
ori(csr, csr, 0x0001); // ENABLE = 1
iret();
の順で書くか、コンテキスト復帰関数 (os_restore_context) の中で csr を復元してから iret します。
まとめ
- ハードウェア: 受理時に PC→ira / ENABLE=0 / PC=0x0004 / FIREi=1。これだけ
- ソフトウェア (RTOS 側): 退避 → ディスパッチ → ハンドラ → フラグクリア → スケジューラ → 復帰 →
iret - ソフトウェア (ユーザ側):
intr0()-intr3()を書く。発火フラグをクリアする
関連項目
- 割り込み関連の命令 (
iret) の機械語仕様: 命令セットアーキテクチャ - CSR の
andi/ori1 命令制御や、レジスタ規約: 関数呼び出し規約 - 退避された TCB とスケジューラの実装: RTOS
- ハードウェア側のタイミング図と回路: マイコン回路