皆さん こんにちは。
今回は5つ目のソースコードに入れ替えて動作確認してみます。
第3部3章のタイトルは「システム・タイマとタスクの時間待ち機能」です。
この章ではシステム・タイマとタスク遅延APIなどの機能が追加されました。
この記事は開発環境を構築することを前提にしています。
開発環境を構築したい方は ゼロから作るOS 環境構築編 をご覧になってください。
この記事はインターフェース誌2023年7月号の「ゼロから作るOS」を参考にしています。
まずは書籍の本章を読んで予習しておくことをお薦めします。
ソースコードを入れ替える
これから前回製作したソースを削除するので、必要ならバックアップをとっておいてください。
Project Explorerでpico_tryknlのツリー下のフォルダを全て選択し、右クリックして Delete を選択して削除します。

次にソースファイルをIDEにコピーします。
C:\DevTools\src\IF2307TK\part_3\sect_3 下にある以下のフォルダを全て選択して、IDEの Project Explorer の pico_tryknl にドラッグ&ドロップします。
(書籍 第3部3章のソースファイルになります)
次のウィンドウが出るので、そのままOKを押してコピーします。

コピーしたフォルダを展開し、フォルダ下にもファイルがコピーされていれば成功です。

PC側でTera Termの設定を行う
この章のコードでは途中経過をUARTで送信するようになっているので、PC側で受信した文字列を表示する準備をしておきます。
PCアプリのTera Termを起動し新しい接続画面でシリアルを選択し、ポートに適切なCOMポートを選択します。
Tera Termをお持ちでない方はサイトからダウンロードしてインストールしてください。

設定 – シリアルポート でスピートを 115200 に設定します。(それ以外は以下の設定)

設定 – 端末 で改行コード 受信・送信を LF に設定します。(改行して表示を見やすくします)

プログラムを動かしてみる
Run – Debug History から pico_tryknl Debug を選択しResumeボタンが緑色になったらボタン(またはF8キー)を押してプログラムを実行します。
Tera Termに以下の文字列が表示され、Lチカしていれば成功です。
(1秒毎に hello が出力されます)

プログラム概要
この章のプログラムの概略説明です。
つくられるタスクは3つあり、main()で最初のタスク(初期タスク)が生成されます。
その後、tk_sta_task()で初期タスクが実行されます。
このタスクの優先順位は 1 に設定されていて、つくられるタスクの中では一番高くなっています。
初期タスク initsk() では “Start Try Kernel”の文字列をUARTで送信します。
その後、usermain()関数を実行して tk_ext_tsk() で休止します。
usermain()内では、タスク1とタスク2が生成されて実行されます。
実行されると書きましたが実際にはキューに入れるだけです。
タスク1とタスク2の優先順位は 10 で 1 よりも低いため、初期タスクが休止するまでこれらのタスクは実行されません。
(数値が小さいほど優先順位は高い)
その後、tk_ext_tsk()により初期タスクが休止すると、タスク1が動き出します。
tk_ext_tsk()で初期タスクを休止した後スケジューラーが動くためです。
スケジューラーは次に実行すべきタスクを決定しディスパッチ(タスクの切替)を行います。
タスク1と2は同じ優先順位ですが、スケジューラーは先に生成したタスクから実行します。
タスク1は 0.5秒周期でLチカします。
tk_dly_tsk()によって待ちに入るためタスク2に処理が移ります。
タスク2は 1秒周期で”hello” の文字列をUARTで送信します。
tk_dly_tsk()によって待ちに入るためタスク1に処理が移ります。
いよいよ本格的にディスパッチが動き始めました。
それでは追加された機能のコードを見ていきます。
タスクの実行遅延API
タスク内で単に時間待ちをすることは原則的に禁止です。
その間、優先順位の低いタスクが実行されくなってしまうからです。
そこで時間待ちの間にプログラムの実行権を他のタスクに譲る機能を実装します。
次のtk_dly_tsk()です。
/* タスクの実行遅延 API */
ER tk_dly_tsk( RELTIM dlytim )
{
UINT intsts;
ER err = E_OK;
DI(intsts); // 割込みの禁止
if(dlytim > 0) {
tqueue_remove_top(&ready_queue[cur_task->itskpri]); // タスクをレディキューから外す
/* TCBの各種情報を変更する */
cur_task->state = TS_WAIT; // タスクの状態を待ち状態に変更
cur_task->waifct = TWFCT_DLY; // 待ち要因を設定
cur_task->waitim = dlytim + TIMER_PERIOD; // 待ち時間を設定
cur_task->waierr = &err; // 待ち解除時のエラーコード
tqueue_add_entry(&wait_queue, cur_task); // タスクをウェイトキューに繋ぐ
scheduler(); // スケジューラの実行
}
EI(intsts); // 割込みの許可
return err;
}
処理概要:
割り込みを禁止にしてレディ・キューから実行中のタスクを削除します。
TCBのstateをTS_WAITに設定した後に待ち時間を設定し、タスクをウェイト・キューに登録します。
そしてスケジューラーを実行した後に割り込みを許可します。
戻り値はER型のエラーコードですが、本章ではこの値を気にしなくて良さそうです。
(エラーが返ることはない)
ウェイト・キューというものが出てきました。
これもレディ・キューと同じ TCB*型です。
レディ・キューの入れ物は配列でしたが、こちらは配列ではありません。
ウェイト・キューは優先順位に関係なく使えるので配列ではありません。
レディ・キューと同じ関数で扱うようになっていて、ウェイト・キューも pre と next による双方向の連結リストになっています。
レディ・キューと同様にwait_queueには先頭の要素が入ります。
待ちに入るところで、レディ・キューからタスクを削除し、ウェイト・キューに入れます。
指定時間が経過したらウェイト・キューからタスク削除し、レディ・キューの終端にタスクを登録します。
tk_dly_tsk()を呼ぶと、このようにして実行状態から待ち状態に入り、指定時間経過後に実行可能状態へと遷移します。
システム・タイマ
RTOSの時間管理の機能を実現するのがシステム・タイマです。
システム・タイマを使って指定した時間タスクを待ち状態にするAPIを実現します。
ベクターテーブルに systimer_handler を追加し、システムタイマの初期化コードを変更することでタイマ割り込みが動くようになります。
システムタイマの初期化コードは boot\reset_hdr.c の init_systim() を参照してください。
割り込みハンドラは kernel\systimer.c の systemer_handler() です。
/* システムタイマ割込みハンドラ */
void systimer_handler(void)
{
TCB *tcb;
for( tcb = wait_queue; tcb != NULL; tcb = tcb->next) {
if(tcb->waitim == TMO_FEVR) {
continue;
} else if(tcb->waitim > TIMER_PERIOD) {
tcb->waitim -= TIMER_PERIOD; // 待ち時間から経過時間を減じる。
} else { // 待ち時間が経過したタスクを実行できる状態に戻す
tqueue_remove_entry( &wait_queue, tcb); // タスクをウェイトキューから外す
*tcb->waierr = E_OK;
tcb->state = TS_READY;
tcb->waifct = TWFCT_NON;
tqueue_add_entry( &ready_queue[tcb->itskpri], tcb); // タスクをレディキューにつなぐ
}
}
scheduler(); // スケジューラを実行する
}
処理概要:
forループでウェイト・キューに連結された全てのリストを走査し、以下の処理を実行します。
if(tcb->waitim == TMO_FEVR) {
continue;
}
waitimメンバが TMO_FEVR なら continue します。(永遠の時間待ちなので処理なし)
else if(tcb->waitim > TIMER_PERIOD) {
tcb->waitim -= TIMER_PERIOD; // 待ち時間から経過時間を減じる。
}
waitmメンバが TIMER_PERIOD(10msec) より大きければ
指定待ち時間から経過時間(10msec)を差し引きます。
else {
tqueue_remove_entry( &wait_queue, tcb); // タスクをウェイトキューから外す
*tcb->waierr = E_OK;
tcb->state = TS_READY;
tcb->waifct = TWFCT_NON;
tqueue_add_entry( &ready_queue[tcb->itskpri], tcb); // タスクをレディキューにつなぐ
}
指定時間が経過したのでタスクをウェイト・キューから削除してstateをTS_READYに変更し、タスクをレディ・キューに登録します。
forループ完了後、スケジューラーを実行します。
クリティカル・セクション
書籍の78ページに書かれている部分ですが大切なところなので書いておきます。
プログラムがどこで動いていても、システム・タイマ割り込みは発生する可能性があります。
タスクに関わるAPIではTCBやキューなどOS資源への操作を行っているため、その途中で割り込みが入るとOS資源への競合が発生して不具合が起こる可能性があります。
このように複数の処理が重なり不具合が起こる可能性のある部分をクリティカル・セクション(危険領域)と言います。
不具合を回避するためにAPIのクリティカル・セクションの入口で割り込みを禁止し、出口で許可しています。
ディスパッチ中に実行可能なタスクがない場合、dispatch_entry(アセンブラのコード)内でアイドル・ループを実行します。
このアイドル・ループ内では一時的に割り込みを許可する必要があります。
タスクを実行状態に移すのはシステム・タイマ割り込みで行うためです。
しかしアイドル・ループ中に実行されたシステム・タイマの処理からディスパッチが実行されるとディスパッチが重なってしまいます。
これを回避するために disp_running 変数(ディスパッチが実行中であることを示す変数)を使って、スケジューラーは disp_runninng 中には dispatch() を実行しないようにしています。
今回は待ちのAPIを使ってタスクが切り替わるしくみを学びました。
だんだん面白くなってきました。皆さんはいかがでしたか?
お疲れさまでした。
この記事の続きは こちら です。