tasm のみで書かれた小さな RTOS です。OS 本体は init.tasm / task.tasm / heap.tasm / serial.tasm の 4 ファイルで、ユーザ側からは init_tasks() と intr0()-intr3() の 5 関数を実装すれば動きます。
全体構成
| ファイル | 役割 |
|---|---|
init.tasm | リセットベクタ、割り込みベクタ、init()、_exit |
task.tasm | TCB / 割り込みプロローグ / スケジューラ |
heap.tasm | bump allocator (malloc) |
serial.tasm | print_char / print_str / print_hex |
defaults.tasm | ユーザ側シンボルの空 stub |
メモリレイアウト
アーキテクチャ仕様に沿って、固定アドレスに OS のグローバル変数を配置します。
| Addr | Var | 用途 |
|---|---|---|
0x0000:0x000F | レジスタ | (アーキテクチャ仕様) |
0x0010 | HALT | 1 を書くと CPU クロック停止 |
0x0040 | heap_top | ヒープの先頭ポインタ |
0x0041 | heap_bottom | ヒープの末尾ポインタ |
0x0042 | task_running | 実行中の *TCB |
0x0043 | task_list | TCB 連結リストの先頭 |
0x0044:〜 | ユーザ領域 | user_global_size でユーザが宣言する分 |
〜:0xDFFF | ヒープ | bump allocator が伸びていく |
0x1000:0x1001 | MMIO | serial_tx / serial_rx |
ブートと終了
リセットベクタはアドレス 0x0000 に固定で配置します。
asm @ 0x0000 reset {
loadi(fp, 0xffff); // FP のみがスタックポインタを兼ねる
call(init);
halt:
jumpr(halt);
}
asm @ 0x0004 irq {
jump(_intr);
}
init() は次の順で起動処理を行います。
heap_top/heap_bottomの初期化task_running/task_listを null クリア- ユーザ実装の
init_tasks()呼び出し os_task_scheduler()へジャンプ (戻らない)
全タスクが exit したらスケジューラから _exit にジャンプし、CSR の clock_stop ビット (0x0100) を立てて CPU クロックを停止します。
ABI (Application Binary Interface)
呼出規約の仕様は 関数呼び出し規約 を参照。本記事では RTOS 内部から見て押さえておきたい部分だけ要約します。
spは廃止。スタックはfpのみで管理- 全レジスタが caller-save (
t0-t9、ra含む) - 引数 / 戻り値はすべてスタック経由。callee 視点では
fp+0=saved ra、fp+1=戻り値、fp+1+i=引数 i、fp-1=saved caller-FP、fp-2..=ローカル
タスク管理
TCB (Task Control Block)
TCB はわずか 5 ワードのシンプルな構造体です。優先度やフラグマスクのような高機能スケジューリングは実装していません。
type TCB = {
next: *void, // 次のタスク (実体は *TCB、cast で復元)
fp: *int, // 中断時の FP (= 退避領域の下端)
bp: *int, // スタック上端
ra: *int, // 中断時の IRA (= 再開時の PC)
state: int, // 0=ready, 1=waiting, 2=exit
};
TCB は task_list (0x0043) を先頭とする単方向連結リストで管理し、task_running (0x0042) が現在実行中の TCB を指します。
退避領域
中断時には、TCB が指す FP を起点に下向きに 12 word の退避領域を取り、レジスタを丸ごと保存します。
| オフセット | 内容 |
|---|---|
fp + 0 | ra |
fp + 1 | 旧 fp |
fp + 2 | t0 |
fp + 3 | t1 |
| … | … |
fp + 11 | t9 |
復帰時は os_restore_context で全て読み出し、最後に iret() で IRA に保存していた再開 PC へジャンプします。
タスクのセットアップ
fn os_task_setup(initial_state: int, stack_size: int, entry_address: int) -> *TCB
task_listを末尾まで辿るmalloc(sizeof<TCB>)とmalloc(stack_size)で TCB とスタックを確保- スタック上端から下に 12 word ぶんを退避領域として使い、初期 FP を計算
- TCB を連結リストに追加し、
fp / bp / ra / stateを初期化 - 退避領域に初期コンテキスト (RA = entry_address, 旧 FP = stack_top, T0-T9 = 0) を仕込む
タスクの一時停止
asm os_task_wait { ... }
- 割り込み禁止 (
andi(csr, csr, 0xfffe)) - レジスタを退避領域へ保存
- TCB の
fp/ra/state = waitingを更新 os_task_schedulerへジャンプ
タスクの再開
asm os_task_ready(target: *TCB) { ... }
- 引数
targetを読み出す (FP を動かす前に) - 現在のタスクを退避
target.state = readyに書き換えos_task_schedulerへジャンプ
タスクの終了
asm os_task_exit { ... }
現在のタスクの state = exit にセットしてスケジューラへジャンプ。退避はしません。
スケジューラ
スケジューラは協調型です。task_list を先頭から走査して、state == ready の最初のタスクを実行します。
asm os_task_scheduler { ... }
task_listから順に TCB を辿るstate == readyを見つけたらtask_runningに書いてos_restore_contextへ- ready が一つも無ければ
_exitへ
割り込み
割り込みのハードウェア仕様 (CSR / 発火フラグ / 受理タイミング) と例外モデルは 割り込み処理 を参照。ここでは RTOS が用意するディスパッチ層の中身に絞ります。
割り込みベクタ (0x0004) は _intr にジャンプします。
asm _intr {
subi(fp, fp, 12); // 退避領域を確保
store(ra, fp, 0); // ra と t0-t9 を退避
store(t0, fp, 2);
...
store(t9, fp, 11);
addi(t0, fp, 12); // 中断時の FP を slot 1 に書く
store(t0, fp, 1);
load(t0, z, task_running@);
store(ira, t0, (@(0 as *TCB)).ra@);
store(fp, t0, (@(0 as *TCB)).fp@);
jump(intr_dispatch);
}
intr_dispatch() は CSR の発火フラグ (bit 8-11) を見て、立っていれば対応するユーザハンドラ (intr0()-intr3()) を呼びます。
fn intr_dispatch() {
if ((csr & intr0_flag) != 0) { intr0(); }
if ((csr & intr1_flag) != 0) { intr1(); }
if ((csr & intr2_flag) != 0) { intr2(); }
if ((csr & intr3_flag) != 0) { intr3(); }
os_task_scheduler();
}
ユーザハンドラの中で csr = csr & ~flag のように発火フラグをクリアし、最後にスケジューラへ渡します。
時分割スケジューリングの作り方
ハードウェアタイマからの周期的な 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; // intr0 フラグをクリア
}
メモリ管理
bump allocator
fn malloc(size: int) -> *int {
var addr: *int = heap_top;
heap_top = (addr as int + size) as *int;
return addr;
}
現状はメモリ解放を行わない単純な bump allocator です。os_heap_init / os_heap_aloc / os_heap_free は将来の高機能ヒープ用に枠だけ用意してあります。
シリアル I/O
MMIO の serial_tx / serial_rx を介して文字列を送受信します。
fn print_char(c: int) {
serial_tx = c;
}
fn print_str(s: *int) {
var p: *int = s;
while (@p != 0) {
serial_tx = @p;
p = (p as int + 1) as *int;
}
}
print_hex は 16bit 整数を 4 桁の 16 進数として送信します。
排他制御
os_lock_init / os_lock_aloc / os_lock_free は未実装です。協調型なので os_task_wait / os_task_ready で簡易な待ち合わせは書けます。