Kanade Electronicskanaelecom

RTOS

tasm のみで書かれた小さな RTOS です。OS 本体は init.tasm / task.tasm / heap.tasm / serial.tasm の 4 ファイルで、ユーザ側からは init_tasks()intr0()-intr3() の 5 関数を実装すれば動きます。

全体構成

ファイル役割
init.tasmリセットベクタ、割り込みベクタ、init()_exit
task.tasmTCB / 割り込みプロローグ / スケジューラ
heap.tasmbump allocator (malloc)
serial.tasmprint_char / print_str / print_hex
defaults.tasmユーザ側シンボルの空 stub

メモリレイアウト

 アーキテクチャ仕様に沿って、固定アドレスに OS のグローバル変数を配置します。

AddrVar用途
0x0000:0x000Fレジスタ(アーキテクチャ仕様)
0x0010HALT1 を書くと CPU クロック停止
0x0040heap_topヒープの先頭ポインタ
0x0041heap_bottomヒープの末尾ポインタ
0x0042task_running実行中の *TCB
0x0043task_listTCB 連結リストの先頭
0x0044:〜ユーザ領域user_global_size でユーザが宣言する分
〜:0xDFFFヒープbump allocator が伸びていく
0x1000:0x1001MMIOserial_tx / serial_rx

ブートと終了

 リセットベクタはアドレス 0x0000 に固定で配置します。

asm @ 0x0000 reset {
    loadi(fp, 0xffff);  // FP のみがスタックポインタを兼ねる
    call(init);
  halt:
    jumpr(halt);
}

asm @ 0x0004 irq {
    jump(_intr);
}

 init() は次の順で起動処理を行います。

  1. heap_top / heap_bottom の初期化
  2. task_running / task_list を null クリア
  3. ユーザ実装の init_tasks() 呼び出し
  4. os_task_scheduler() へジャンプ (戻らない)

 全タスクが exit したらスケジューラから _exit にジャンプし、CSR の clock_stop ビット (0x0100) を立てて CPU クロックを停止します。

ABI (Application Binary Interface)

 呼出規約の仕様は 関数呼び出し規約 を参照。本記事では RTOS 内部から見て押さえておきたい部分だけ要約します。

  • sp は廃止。スタックは fp のみで管理
  • 全レジスタが caller-save (t0-t9ra 含む)
  • 引数 / 戻り値はすべてスタック経由。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 + 0ra
fp + 1fp
fp + 2t0
fp + 3t1
fp + 11t9

 復帰時は os_restore_context で全て読み出し、最後に iret() で IRA に保存していた再開 PC へジャンプします。

タスクのセットアップ

fn os_task_setup(initial_state: int, stack_size: int, entry_address: int) -> *TCB
  1. task_list を末尾まで辿る
  2. malloc(sizeof<TCB>)malloc(stack_size) で TCB とスタックを確保
  3. スタック上端から下に 12 word ぶんを退避領域として使い、初期 FP を計算
  4. TCB を連結リストに追加し、fp / bp / ra / state を初期化
  5. 退避領域に初期コンテキスト (RA = entry_address, 旧 FP = stack_top, T0-T9 = 0) を仕込む

タスクの一時停止

asm os_task_wait { ... }
  1. 割り込み禁止 (andi(csr, csr, 0xfffe))
  2. レジスタを退避領域へ保存
  3. TCB の fp / ra / state = waiting を更新
  4. os_task_scheduler へジャンプ

タスクの再開

asm os_task_ready(target: *TCB) { ... }
  1. 引数 target を読み出す (FP を動かす前に)
  2. 現在のタスクを退避
  3. target.state = ready に書き換え
  4. os_task_scheduler へジャンプ

タスクの終了

asm os_task_exit { ... }

 現在のタスクの state = exit にセットしてスケジューラへジャンプ。退避はしません。

スケジューラ

 スケジューラは協調型です。task_list を先頭から走査して、state == ready の最初のタスクを実行します。

asm os_task_scheduler { ... }
  1. task_list から順に TCB を辿る
  2. state == ready を見つけたら task_running に書いて os_restore_context
  3. 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 のように発火フラグをクリアし、最後にスケジューラへ渡します。

時分割スケジューリングの作り方

 ハードウェアタイマからの周期的な intr0task_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 で簡易な待ち合わせは書けます。