GR-SAKURAではじめる超お手軽マルチタスク setjmpとlongjmpの利用 [RX&SH&H8]
これはマルチタスクではあってもリアルタイムではありません!。
今から紹介するのは、全てのタスクが同一、、、と言うより優先度の制御が入っていない
マルチタスクシステムであり、μITRONの様にユーザーが意識しなくても優先度に応じて
カーネルが自動的にタスク切り替えを行いません。と言うかカーネルそのものが無いです。
上記欠点はありますが、setjmp、longjmpと言ったC言語でサポートされている関数ライブ
ラリを利用できる点や、タスクを起動する為の初期化手順が単純なため、GR-SAKURA上の
動作環境(WEBコンパイラで実現できる範囲内)でマルチタスクを実現できるメリットが有り
ます。
また、タスクを起動する為の手順が判ってしまえば別のマイコンへの応用も容易です。
1.今一度、割り込みの手順を考察(※RX63Nの汎用割り込みの場合)
(1)命令実行中に割り込み要求を受付
(2)一部の命令を除いて実行中の命令を完了後、割込み処理を開始
(3)PSW(ステータスレジスタ)をスタックに保存
(4)PC(プログラムカウンタ)をスタックに保存
(5)割り込みフラグ等のCPUステータスを変化させる
(6)割り込み処理ルーチンにジャンプ
(7)割り込み処理ルーチン内で必要なレジスタをスタックに退避する
※1~6まではハードウエアが実行し、7からは割り込み処理ルーチンの責任でソフトウ
エアで実行します。
割込時のスタックの状況を図にしてみると図の様になります。
事実上、スタック領域Aと、スタック領域Bは別の目的の為に使用されていて、相互に関係を
持ちません。これらのスタック領域は、物理的には別空間ではなく連続空間にあるけれど、
概念的には別空間にあると考える事ができます。
2.割込とマルチタスクの共通点
それぞれ使用目的別にスタック領域を使い分ける概念は、割り込みとマルチタスクで共通し
ています。言い換えれば割り込みの手続きと同じ事ができればスタック領域を使い分ける事が
でき、ひいてはマルチタスクシステムを実現できます。
※仮想メモリ空間を持たないマイコンの場合の話
実際のマルチタスクシステムのスタックの例です。
※マルチタスクシステムによっては割り込みスタック領域も別に用意する
マルチタスク?だがRXマイコンはCPUが一つしか無い
マルチコアでないマイコンでマルチタスクシステムを実現?現実的には一つのCPUコアは一つ
の命令しか実行できません。マルチタスクでそれぞれの処理が平行に動いている様に思えるけ
れど、実際はCPUで実行する処理を適宜切り替えてるだけで、同時に処理は行っていません。
一般的なマルチタスクシステムの処理の切り替え手順
タスク1からタスク2へ切り替え手順を解説。※ここでは関数コールで切り替えを実現している
為、PSWは保存しない。
(1)まずタスク1の中でタスク切り替えを行う専用の関数をコールする。
(2)その関数の中でPCや汎用レジスタの一切をタスク1専用のスタック領域に退避する。
(3)SP(スタックポインタ)をタスク2のSPに書き換えてしまう。この時、SPはタスク2のス
タック領域のアドレスを示している。
(4)タスク2のスタック領域に退避されている汎用レジスタを書き戻す。
(5)PCにタスク2で実行していたアドレスを代入する事でタスク2の実行が再開される。
※関数からのRETで実現※PCや汎用レジスタの退避やロードと言った処理は、割り込み
の出入り口処理と同じである事が判る。
3.setjmp、longjmpの利用
μITRON等のRTOSは、初期化手順がちょっと難易度が高い。判ってしまえば何等問題無いので
すが。また、タスクの切り替え処理は基本的にCPUの特権モードで行われるので、割り込みが自
由に使えないといけない。しかしC言語にはPCや汎用レジスタを指定した領域に退避したり、指
定した領域に退避されているレジスタを書き戻す関数が大昔から用意されています。それが
setjmp、longjmpで、これを利用すれば簡易的なマルチタスクシステムを実現できます。
※但し割り込みで切り替える訳ではないのでリアルタイム性の実現は困難
setjmpが退避する関数であり、longjmpが退避された値をレジスタに書き戻します。
以下はsetjmpの逆アセンブル結果で、片っ端から汎用レジスタをR1で示されるメモリに代入し
ている事が判ります。
うーん、イマイチ効率の良いコードではない!気がしますね。
以下は同じgccですが某マイコンアーキテクチャのsetjmp、longjmpです。命令でバレバレですが。
実際のところRXマイコンにも汎用レジスタをまとめて退避、ロードする命令は有りますし、
関数コールで実現しているので、必ずしも全てのレジスタを退避する必要は無い筈です。
まあ今回はおいて置きます。
汎用レジスタR1は退避先のアドレスであり、汎用レジスタR0はRXマイコンの場合はスタックポイ
ンタを示します。※赤枠
また、関数コールを行っている事からPCは既にスタックに退避されています。つまりタスク1の
スタックと、レジスタの退避領域は以下の様になっています。
以下はlongjmpの逆アセンブル結果で、片っ端から退避した値を汎用レジスタに書き戻している
事が判ります。
汎用レジスタR1は退避先のアドレスであり、赤枠に注目すると最初にスタックポインタを元に戻
しています。次に退避したPCの値をスタックポインタが示すアドレスに代入しています。これで
関数から返る時に実行先のPCの値が代入され、中断していた処理が再開される事となります。
ちなみに汎用レジスタを保存する領域、jmp_bufって何?と調べてみるとgccでは以下の様になっ
ています。
#ifdef __RX__ #define _JBLEN 0x44 #endif #ifdef _JBLEN #ifdef _JBTYPE typedef _JBTYPE jmp_buf[_JBLEN]; #else typedef int jmp_buf[_JBLEN]; #endif #endif
なんて事無いint型の配列ですが、その配列サイズが問題ですね。0x44!本来なら汎用レジス
タとPCの分だけ領域が有ればいい筈ですが、、、0x44は10進で68ですから、17×4byte=68byte
と勘違?
setjmp、longjmpのコーディング例
#includejmp_buf tsk_env; setjmp(tsk_env); longjmp( tsk_env, 1 );
と、こう書いてしまうとsetjmpとlongjmpの間で無限ループになってしまいます。そこで以下の
様に書く必要があります。
#includejmp_buf tsk_env; setjmp(tsk_env); 何らかの待ち状態を解除する条件式; longjmp( tsk_env, 1 );
例えばタイマー割り込みでカウントアップされる変数を条件にすると以下の様になります。
#includejmp_buf tsk_env; void wai_systim_eq_100() { setjmp(tsk_env); If( systim >= 100 ) return; longjmp( tsk_env, 1 ); }
実際のところマイコンの処理のほとんどは「何かを待つ」です。時間であったり、AD変換結
果であったり、シリアルからの受信であったり、送信バッファの空き待ちであったりと、とに
かく待ってばかりです。この何かを待つ間、CPUが無駄にループするのではなく、別の処理を
行ってしまえば、たとえ高速でないマイコンであっても効率良く仕事をさせる事ができますし、
複数の処理を平行して行いたい時、大概の場合シングルタスクでは上手く構成できない事が多
いです。
例えば先のsetjmp、longjmpのコーディング例では条件が一致するまでループを継続していま
したが、条件が一致しないなら別のタスクに処理を切り替えてしまえば良いわけです。
#includejmp_buf tsk1_env,tsk2_env; void task1() { for(;;) { //何かの処理 dly_tsk_100(); } } void dly_tsk_100() { long long tim = systim + 100; setjmp(tsk1_env); //今居る位置の状態を保存 If( systim >= tim ) return; //待ち状態を解除する式 longjmp( tsk2_env, 1 ); //task2にジャンプする } void task2() { for(;;) { //何かの処理 dly_tsk_200(); } } void dly_tsk_200() { long long tim = systim + 200; setjmp(tsk2_env); //今居る位置の状態を保存 If( systim >= tim ) return; //待ち状態を解除する式 longjmp( tsk1_env, 1 ); //task1にジャンプする }
4.如何にしてタスクを起動するか
setjmpは実行中、setjmpが呼ばれた時のコンテキストを保存する関数ですし、longjmp
は保存されたコンテキストを展開する関数です。
マルチタスクに適用する時問題になるのが、如何にしてタスクを起動するか?となります。
タスクを起動する前に初期化処理の中でsetjmpを呼んだとしても、その初期化処理の位置
を覚えてしまうに過ぎません。さて、、、
そこでjmp_bufの中身をもう一度思い出してみます。
setjmpの逆アセンブル結果から保存領域の先頭にスタックポインタ、最後にプログラムカ
ウンタ(PC)を保存していました。
つまり予め用意された保存領域の先頭と最後にタスク用として確保したスタック領域の
再下端とタスクの開始アドレスを保存して、longjmpを行えばそこに飛ぶ事になります。
以下の様な処理を書いてみました。
/****************************************************************************/ /* タスク初期化 */ /****************************************************************************/ ER reg_tsk( ID tid, void (*task)(void), void *stk, unsigned int sz ) { if( tid < 0 || tid >= MAX_TASK_NUMBER ) return E_ID; tsk_env[tid][0] = (int)stk + sz - 8; //タスクのスタックを登録 tsk_env[tid][16] = (int)task; //タスクのエントリーポイントの登録 rdy_flg[tid] = TTS_DMT; return E_OK; }
(1)それぞれのタスクには予めID番号を振り分けます。
(2)tsk_envはsetjmpの保存領域で、タスクの数だけ確保します。
(3)スタックポインタの代入される領域にタスク専用のスタックの開始アドレスを
代入します。
(4)PCの代入される位置にタスクの開始アドレスを代入します。
(5)それぞれのタスクの起動状態を代入します。まだ起動していないのでDORMANTと
しています。
※スタックポインタと開始アドレス以外の汎用レジスタの値はどうでもいいです。
この時点では実行に関係しません。
全てのタスクがCPUの起動直後に起動するとは限らないので、起動が必要なタスクの
状態をREADYに変化させる関数を書いてみました。
/****************************************************************************/ /* タスク開始 */ /* 実際はスケジューラーに登録 */ /****************************************************************************/ ER sta_tsk( ID tid ) { if( tid < 0 || tid >= MAX_TASK_NUMBER ) return E_ID; rdy_flg[ tid ] = TTS_RDY; //タスク登録 return E_OK; }
これを初期化処理の中に記載し、初期化処理が完了する時に以下の関数を呼びます。
/****************************************************************************/ /* ラウンドロビン開始 */ /****************************************************************************/ void sta_rdq( ID tid ) { cur_tid = tid; longjmp( tsk_env[ tid ], 0 ); // }
マルチタスクのサンプルを以下のリンク先において置きます。
https://dl.dropbox.com/u/60463387/grsakura/start_multitask.zip
5.問題は
タスク自身が他のタスクへの切り替えを意識して行う必要があります。
何かを待つ時、常にタスクの切り替え処理を入れておきましょう。
6.もう少しまともなマルチタスクが良いなら
HOS(Hyper Operating System)はμITRON4.0仕様のRTOSであり、無償で利用可能です。
すでにRXマイコンの実装依存部が提供されています。
http://sourceforge.jp/projects/hos/
ITRONプログラミング入門―H8マイコンとHOSで始める組み込み開発
- 作者: 濱原 和明
- 出版社/メーカー: オーム社
- 発売日: 2005/04/25
- メディア: 単行本
2012-12-23 17:34
nice!(0)
コメント(0)
トラックバック(0)
コメント 0