So-net無料ブログ作成

GR-SAKURAではじめる超お手軽マルチタスク2 setjmpとlongjmpを捨てる刻 [RX&SH&H8]

前回のマルチタスクではC言語で標準サポートされているsetjmp、longjmpを使って
実現していました。

この方法の良いところは、setjmp、longjmpがC言語で標準でサポートされている事か
ら、どの様なマイコンでも容易に利用できる点です。
しかし逆アセンブルの結果や、そもそも目的が異なる点から不満が無い訳でもありま
せんでした。

最大の問題はやはり無駄が多い点で、この無駄がコードサイズやRAMサイズ、実効速度
の足を引っ張る事となります。

まあそうなれば自分でコードを書くしかない訳で、コンテキストを入れ替える処理と
タスクをスタートさせる処理のみアセンブラで記述してみました。
アセンブラと言う事で難しい印象はありますが、極めて限定的な処理のみ記述してい
ます。

以下はタスクをスタートさせる処理です。
/************************************************
コンテキストをロードし、タスクを開始
void sta_ctx(
void *exe ) //実行コンテキストの保存先
************************************************/
.text
.align 2
_sta_ctx:
mov.l 0[r1],r0 /*スタックポインタ設定*/
popm r6 - r14 /*r6-r14を復帰*/
rts

リターン命令を入れても、3行でできます。

以下はコンテキストを切り替える処理です。
/************************************************
実行コンテキストの切替
void swi_ctx(
void *pre, //現在のコンテキストの保存先
void *post) //切り替えるコンテキスト
************************************************/
.text
.align 2
_swi_ctx:
pushm r6 - r14 /*r6-r14を退避。r6が最も若いアドレスになる*/
mov.l r0,0[r1] /*スタックポインタ保存*/
mov.l 0[r2],r0 /*スタックポインタ復帰*/
popm r6 - r14 /*r6-r14を復帰*/
rts

リターン命令を入れても、5行でできます。

この2つをC言語から呼び出しています。つまりスタックやレジスタの操作のみ
アセンブラで記述し、それ以外は全てC言語で記述できます。

setjmp、longjmpを使った方法と異なり、今回の方法は汎用レジスタは
タスクスタックに保存しています。スタックポインタのみ大域変数領域に保存しま
す。
ちょうど、以下の図と同じ展開となります。
multitask_004.png


setjmpでは決められたアドレスに決められた手順で汎用レジスタが退避されますが、
今回の方法はスタックに退避される為、汎用レジスタの具体的な退避アドレスを予
め知る事は困難です。
その為スタックポインタのみ、アドレスが判明している領域に保存するのです。

言い換えれば宝物を何処かに埋めて、その地図を作成する様な物です。
宝物を掘り返す時は地図でその位置を見付けます。カリブの海賊みたいな話ですネ。

スタックに退避する長所は、複数レジスタを一発で退避、呼出しできる命令を使え
る点です。
「popm r6 - r14」とか「pushm r6 - r14」がそれで、R6~R14までの汎用レジスタを、
スタックポインタが示す位置から1命令で退避や呼出しします。
その他、PCも自動的にスタックに退避され、また、スタックから読み出しを受けるの
で、特殊な領域を用意してそこに保存するより、スタックに保存する方が全体的に
自然な感じです。

またR6~R14のレジスタのみ退避する理由は、おそらくこのコンパイラでは関数の
呼出しで値の保障が必要とされるレジスタがR0、R6~R14だと思われます。
他のレジスタの値は関数の前後で保障する必要は無い筈です。

必要なレジスタのみ退避する事で、処理時間の短縮や必要なメモリサイズの削減が
できます。

今回の改造ではコンテキストの切り替えの最適化以外に、起動時引数を最大4個まで
渡せる様にしました。決して綺麗な方法ではありませんが、この機能が有ると様々
な目的で使用する事ができます。
以下の様な関数でタスクを登録する時、この起動時引数を指定します。
ER reg_tsk( ID tid, void *tsk, void *stk, int stk_sz,
VP_INT exinf1, VP_INT exinf2, VP_INT exinf3, VP_INT exinf4 );
※記述例
reg_tsk( ID_LED1Task, (void *)LEDTask, tsk4_stk, sizeof(tsk4_stk),
100,PIN_LED0,0,0 );


タスクID、タスクの起動アドレス、スタック領域アドレス、スタックサイズに
続いて引数を渡します。

タスクのを以下の様に記述できればスマートだったのですが、

void LEDTask( VP_INT exinf1, VP_INT exinf2, VP_INT exinf3, VP_INT exinf4 );

ちょっと上手い方法が思いつきませんでした。
以下の様な方法で起動時引数をタスクの中で得る事ができます。
VP_INT exinf[2];

get_par( exinf, 2 ); //起動時引数を取り込んでいる

pinMode( exinf[1], OUTPUT ); //LED?。exinf[1]にはLED pin番号が入っている

VP_INT型の配列を宣言してget_par関数の引数とします。数字の2は引き取りたい引数の
数です。
pinModeで配列の一部のデータを引数に渡しています。このデータはPIN番号です。
※上記の記述例を参照

起動時引数利用の一例を挙げて置きます。
以下はLEDを点滅させるだけのタスクの記述例です。GR-SAKURAには4つの青LEDが有りま
すので、それぞれのLEDに付き1つのタスクで制御しています。実に贅沢ですね。
/****************************************************************************/
/* LEDタスク */
/****************************************************************************/
void LEDTask( void )
{
bool OnOff = false;
VP_INT exinf[2];

get_par( exinf, 2 ); //起動時引数を取り込んでいる

pinMode( exinf[1], OUTPUT ); //LED?。exinf[1]にはLED pin番号が入っている

for( ;; ) //関数(タスク)を抜けても行く先が無いので、必ず無限ループにしておく
{
dly_tsk( exinf[0] ); //時間が経過するまで留まる。 exinf[0]には相対待ち時間が入っている
if( OnOff == false )
{
OnOff = true;
digitalWrite( exinf[1], HIGH );
}
else
{
OnOff = false;
digitalWrite( exinf[1], LOW );
}
}
}

このコードだけで4つのタスクを実行しています。
LEDの点滅を行うだけですが、どのLEDを点滅させるのか?、どれ位の周期で点滅させるのか?
そう言ったパラメーターが存在します。

コードは1つしか無いので、4つのパターンを指定するのには引数等でそのパラメータを
渡す必要があります。起動時引数はこの様な目的で使用できます。


タスクスイッチのパフォーマンスを計ってみます。
setjmp、longjmp方式と比べてどれ程高速化しているでしょうか?。

シミュレーターで計測してみるとsetjmp、longjmp方式は37サイクル、新しい方式だと
24サイクルでした。頻繁に呼ばれる処理なので、なるべく少ないサイクル数で処理できれ
ばそれに越した事は無い訳です。市販のRTOSの場合、このタスクの切り替えに掛かる時間
が、商品の指標の一つになっています。

以下のリンク先からプロジェクトを入手できます。

https://dl.dropbox.com/u/60463387/grsakura/multitask2.zip

nice!(0)  コメント(5)  トラックバック(0) 

nice! 0

コメント 5

adventure

これは面白い。ユーザーモードで、マルチタスクを実現している。
GR-Sakuraボードのstandalone環境でためして、動作しました.
by adventure (2013-01-18 22:52) 

hamayan

動作報告有難うございます。初めて反応いただきました。

いま、TSSも実現しようと思っています。
できたら、またここで公開します。

by hamayan (2013-01-19 08:45) 

adventure

ARM Cortex-M3 で動作したので、コードをさらします。
dispatch.s のみの書き換えで動作しました。
ただし、8byte stack align などを考慮していません。
-------------------------
public swi_ctx ;/*実行コンテキストの切替*/
public sta_ctx ;/*コンテキストをロードし、タスクを開始*/

section .text:CODE

;/************************************************
; コンテキストをロードし、タスクを開始
; void sta_ctx(
; void *exe ) //実行コンテキストの保存先
;************************************************/
sta_ctx:
push {lr} ;/*lrを退避*/
ldr r13,[r0] ;/*スタックポインタ設定*/
pop {r4-r12,pc} ;/*r4-r12,pcを復帰*/

;/************************************************
; 実行コンテキストの切替
; void swi_ctx(
; void *pre, //現在のコンテキストの保存先
; void *post) //切り替えるコンテキスト
;************************************************/
swi_ctx:
push {r4-r12,lr} ;/*r4-r12,lrを退避。r4が最も若いアドレスになる*/
str r13,[r0] ;/*スタックポインタ保存*/
ldr r13,[r1] ;/*スタックポインタ復帰*/
pop {r4-r12,pc} ;/*r4-r12,pcを復帰*/

end
-------------------------
スタックの消費傾向が、GR-Sakuraと同様だったので、期待どうりの動作だと思います。
by adventure (2013-01-21 17:58) 

adventure

ARM Cortex-M3 で動作したので、コードをさらします。
dispatch.s のみの書き換えで動作しました。
ただし、8byte stack align などを考慮していません。
-------------------------
public swi_ctx /*実行コンテキストの切替*/
public sta_ctx /*コンテキストをロードし、タスクを開始*/

section .text:CODE

/************************************************
コンテキストをロードし、タスクを開始
void sta_ctx(
void *exe ) //実行コンテキストの保存先
************************************************/
sta_ctx:
push {lr} /*lrを退避*/
ldr r13,[r0] /*スタックポインタ設定*/
pop {r4-r12,pc} /*r4-r12,pcを復帰*/

/************************************************
実行コンテキストの切替
void swi_ctx(
void *pre, //現在のコンテキストの保存先
void *post) //切り替えるコンテキスト
************************************************/
swi_ctx:
push {r4-r12,lr} /*r4-r12,lrを退避。r4が最も若いアドレスになる*/
str r13,[r0] /*スタックポインタ保存*/
ldr r13,[r1] /*スタックポインタ復帰*/
pop {r4-r12,pc} /*r4-r12,pcを復帰*/

end
-------------------------
スタックの消費傾向が、GR-Sakuraと同様だったので、期待どうりの動作だと思います。
by adventure (2013-01-21 18:03) 

hamayan

おお良いですね!。
是非試してみたいと思います。

by hamayan (2013-01-21 18:12) 

コメントを書く

お名前:[必須]
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。

※ブログオーナーが承認したコメントのみ表示されます。

トラックバック 0

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。