皆さん こんにちは。
今回は8つ目のソースコードに入れ替えて動作確認してみます。
第4部3章のタイトルは「複数のタスク間での資源競合を防ぐ…セマフォによる排他制御」です。
この章ではセマフォが追加されました。
セマフォはRTOSで良く使われる機能ですからしくみごと覚えてしまいましょう。
この記事は開発環境を構築することを前提にしています。
開発環境を構築したい方は ゼロから作るOS 環境構築編 をご覧になってください。
この記事はインターフェース誌2023年7月号の「ゼロから作るOS」を参考にしています。
まずは書籍の本章を読んで予習しておくことをお薦めします。
回路を追加する
(すでに回路を追加されている場合には不要です)
スイッチと抵抗(10kΩ)を図のように配線します。(書籍の90ページ参照)
スイッチは押しボタン、タクトスイッチ等を使います。

()内の数字は端子の番号です。以下の図を参考になさってください。
私は赤枠で囲んだ4か所の端子を使いました。

ソースコードを入れ替える
これから前回製作したソースを削除するので、必要ならバックアップをとっておいてください。
Project Explorerでpico_tryknlのツリー下のフォルダを全て選択し、右クリックして Delete を選択して削除します。

次にソースファイルをIDEにコピーします。
C:\DevTools\src\IF2307TK\part_4\sect_3 下にある以下のフォルダを全て選択して、IDEの Project Explorer の pico_tryknl にドラッグ&ドロップします。
(書籍 第4部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には “Start Try Kernel” が表示されます。
その後、スイッチを交互に押すたびに “BTN-0 ON” , “BTN-1 ON”が交互に表示されれば成功です。

プログラム概要
この章のプログラムの概略説明です。
(前回とほぼ同じですが、LEDを制御する部分をセマフォのAPIではさんでいます。そこが前回と違います)
つくられるタスクは今回4つあり、まずmain()で最初のタスク(初期タスク)が生成されます。
その後、tk_sta_task()で初期タスクが実行されます。
このタスクの優先順位は 1 に設定されていて、つくられるタスクの中では一番高くなっています。
初期タスク initsk() では “Start Try Kernel”の文字列をUARTで送信します。
その後、usermain()関数を実行して tk_ext_tsk() で休止します。
usermain()内では、task_btn()とtask_led1()及びtask_led2()が生成されて実行されます。
実行されると書きましたが実際にはキューに入れるだけです。
task_btn()とtask_led1()、task_led2()の優先順位は 10 で 1 よりも低いため、初期タスクが休止するまでこれらのタスクは実行されません。
(数値が小さいほど優先順位は高い)
その後、tk_ext_tsk()により初期タスクが休止すると、task_btn()が動き出します。
tk_ext_tsk()で初期タスクを休止した後スケジューラーが動くためです。
task_btn()では tk_dly_tsk(100)により100msecの時間待ちがあり、実行権を他のタスクに譲ります。
そしてtask_led1()が動き出しますが、tk_wai_flg()により待ち状態に入ります。
そしてtask_led2()が動き出しますが、tk_wai_flg()により待ち状態に入ります。
スイッチ2が押されたことを検出すると “BTN-0 ON” の文字列を UART で送信し、0.5秒間隔でしばらくLチカして消灯します。
スイッチ1が押されたことを検出すると “BTN-1 ON” の文字列を UART で送信し、0.1秒間隔でしばらくLチカして消灯します。
スイッチ1と2を続けて押してもセマフォで排他制御されるために前回と異なりLチカの間隔は変わりません。
それでは追加された機能のコードを見ていきます。
以下の変更は前回説明したものと同じです。
提供されているソースコード(systimer.c)と異なっていたら変更しておいてください。
ソースコードの変更
書籍の83ページ リスト5の変更内容です。
tk_slp_tsk()による時間待ちの場合、タイムアウト時にはwaierrに E_TMOUT を設定します。
/* システムタイマ割込みハンドラ */
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); // タスクをウェイトキューから外す
if (tcb->waifct == TWFCT_DLY) {
*tcb->waierr = E_OK;
} else {
*tcb->waierr = E_TMOUT;
}
tcb->state = TS_READY;
tcb->waifct = TWFCT_NON;
tqueue_add_entry( &ready_queue[tcb->itskpri], tcb); // タスクをレディキューにつなぐ
}
}
scheduler(); // スケジューラを実行する
}
tk_slp_tsk()による待ちでタイムアウトした場合には waierr に E_TMOUT を設定する処理が追加になっています。
セマフォの生成API
tk_cre_sem()
/* セマフォの生成API */
ID tk_cre_sem( const T_CSEM *pk_csem )
{
ID semid;
UINT intsts;
DI(intsts); // 割込み禁止
for(semid = 0; semcb_tbl[semid].state != KS_NONEXIST; semid++);
if(semid < CNF_MAX_SEMID) {
/* セマフォ管理情報の初期化 */
semcb_tbl[semid].state = KS_EXIST;
semcb_tbl[semid].semcnt = pk_csem->isemcnt;
semcb_tbl[semid].maxsem = pk_csem->maxsem;
semid++;
} else {
semid = E_LIMIT;
}
EI(intsts); // 割込み許可
return semid;
}
処理概要:
セマフォを生成します。
割り込みを禁止し、セマフォ管理情報の配列から空きを探します。
配列に状態、初期値、最大値を設定します。
最大値が1なので「バイナリ・セマフォ」です。
空きがなければ semid に上限値エラーである E_LIMIT を設定します。
そして割り込みを許可し、semid を返します。
(私はカウンティングセマフォは使ったことがなく、その用途がよくわかっていません)
セマフォと言うと通常はバイナリ・セマフォの方を指し、排他制御等に使われます。
セマフォの資源獲得API
tk_wei_sem()
/* セマフォの資源獲得API */
ER tk_wai_sem( ID semid, INT cnt, TMO tmout )
{
SEMCB *semcb;
ER err = E_OK;
UINT intsts;
if(semid <= 0 || semid > CNF_MAX_SEMID) return E_ID;
DI(intsts); // 割込み禁止
semcb = &semcb_tbl[--semid];
if(semcb->state == KS_EXIST) {
if(semcb->semcnt >= cnt) { // 現在のセマフォの資源数 ≧ 要求する資源数
semcb->semcnt -= cnt;
} else if(tmout == TMO_POL) { // 資源が足りなく、かつ、待ち時間0の場合
err = E_TMOUT;
} else { // 資源が足りなく、待ち状態に移行
tqueue_remove_top(&ready_queue[cur_task->itskpri]); // タスクをレディキューから外す
/* TCBの各種情報を変更する */
cur_task->state = TS_WAIT; // タスクの状態を待ち状態に変更
cur_task->waifct = TWFCT_SEM; // 待ち要因を設定
cur_task->waiobj = semid; // *** waiobjを追加 ***
cur_task->waitim = ((tmout == TMO_FEVR)? tmout: tmout + TIMER_PERIOD); // 待ち時間を設定
cur_task->waisem = cnt;
cur_task->waierr = &err;
tqueue_add_entry(&wait_queue, cur_task); // タスクをウェイトキューに繋ぐ
scheduler(); // スケジューラを実行
}
} else {
err = E_NOEXS; // 未登録のセマフォ
}
EI(intsts); // 割込み許可
return err;
}
処理概要:
セマフォの資源を獲得します。
もし割り込みを禁止し、stateが登録済みであれば以下を実行します。
もし現在のセマフォの資源数 ≧ 要求する資源数(1)なら semcnt を -1 します。
この場合、資源を獲得できたのでこの関数からすぐに戻り forループを実行することになります。
それ以外でもし資源が足りなくてかつ待ち時間が0なら err に E_TMOUT を設定します。
それ以外なら
タスクをレディ・キューから削除し、TCBの各情報を変更します。
state には TS_WAIT
waifct には TWFCT_SEM
waiobj には セマフォID
waitim には 待ち時間 TMO_FEVR
waisem には cnt
waierr には &err
その後タスクをウェイト・キューに登録しスケジューラーを実行します。
未登録であれば
err に E_NOEXS を設定します。
その後割り込みを許可し、err を返します。
セマフォの資源返却API
tk_sig_sem()
/* セマフォの資源返却API */
ER tk_sig_sem( ID semid, INT cnt )
{
SEMCB *semcb;
TCB *tcb;
ER err = E_OK;
UINT intsts;
if(semid <= 0 || semid > CNF_MAX_SEMID) return E_ID;
DI(intsts); // 割込み禁止
semcb = &semcb_tbl[--semid];
if(semcb->state == KS_EXIST) {
semcb->semcnt += cnt; // 資源の返却
if(semcb->semcnt <= semcb->maxsem) {
for( tcb = wait_queue; tcb != NULL; tcb = tcb->next) { // ウェイトキューのタスクを確認
if((tcb->waifct == TWFCT_SEM) && (tcb->waiobj == semid)) { // *** 条件を追加 ***
if( semcb->semcnt >= tcb->waisem) { // 要求資源数を満たしていれば実行可能状態へ
semcb->semcnt -= tcb->waisem;
tqueue_remove_entry( &wait_queue, tcb); // タスクをウェイトキューから外す
/* TCBの各種情報を変更する */
tcb->state = TS_READY;
tcb->waifct = TWFCT_NON;
tcb->waierr = &err;
tqueue_add_entry( &ready_queue[tcb->itskpri], tcb); // タスクをレディキューへつなぐ
scheduler(); // スケジューラを実行
} else {
break;
}
}
}
} else { // 資源数が最大値を超えた
semcb->semcnt -= cnt;
err = E_QOVR;
}
} else {
err = E_NOEXS; // 未登録のセマフォ
}
EI(intsts); // 割込み許可
return err;
}
処理概要:
セマフォの資源を返却します。
割り込みを禁止し
もしstateが登録済みであれば以下を実行します。
semcnt を +1 します。
semcnt <= maxsem であれば forループを実行します。
ウェイト・キューを走査します。
待ち要因がセマフォでかつセマフォIDが一致していて、かつ要求資源数を満たしていれば実行可能状態に移します。
semcnt を -1 してタスクをウェイト・キューから削除します。
TCBの書く情報を変更します。
state を TS_READY
waifct を TWFCT_NON
waierr を &err
そしてタスクをレディ・キューに登録してスケジューラーを実行します。
それ以外なら
semcnt を -1 して、err に E_QOVR を設定します。
未登録であれば
err に E_NOEXS を設定します。
その後割り込みを許可し、err を返します。
使い方はいたって簡単で排他制御したい資源をバイナリ・セマフォの資源獲得APIと資源返却APIではさむだけです。
tk_wai_sem() // 獲得API
排他制御したい資源(今回はLチカの処理)
tk_sig_sem() // 返却API
OS内部のしくみがわかっていなかった私は、セマフォを使う場合排他制御したい資源を獲得APIと返却APIではさむくらいの知識しかありませんでした。
なるほど確かにセマフォを待つ間はそのタスクを動かせないので他に処理を譲るべきなのですね。
今回も新しい発見ができました。
いぁや Try Kernel って本当に良いものですね。
次回からいよいよ最終第5部です。
センサーをつなぐアプリケーションを見ていきます。
お疲れさまでした。
この記事の続きは こちら です。