Task Switching Program (TSP)
TSPとは
TSP動作原理とファイル構成
TSPの使い方
プログラム作成例
その他の情報
変更履歴
2011年4月5日 追記
2011年4月3日 新規作成
TSPの状態遷移図です。「TPSの動作原理とファイル構成」を参照ください。
TSPとは
<何が出来るのか>
MSP430シリーズは、低消費電力を積極的にサポートするためシステムの動作レベルによりいくつかのLPM(Low Power Mode)を選択できます。
一方、一般的なプログラムは何らかの事象発生をポーリングして判断し、それに基ずいて何らかの機能を実行する形式が多く、ポーリングとLPMの両立が難しくなります。
これに対して、TIはイベントドリブン(Event Driven)型のプログラム形式を推奨しています。
一番簡単なプログラムは、CPUがリセット状態から起動した後、システムイニシャライズしてLPM状態の無限ループに入り、割込み起動のみでプログラム構成する方法です。
等を行うと、起動すべき割込みが無ければその時間帯は全て低消費電力となるLPM状態を保つことが出来ます。
TPS(Task Switching Program)は、これらの機能を少しだけシステム的に補助してユーザープログラム作成を容易にすることを考えて作成した非常に簡単な機能を持つプログラムです。
私は、PIC(PICでの使用例)やARM(LPC1768での使用例)の趣味の電子工作では「FreeRTOS」を使用させていただいています。このRTOSは、非常によく出来ており機能には充分満足しています。
しかし、MSP430の多くのファミリーチップの中で使用できるのは、限られた種類になってしまいます。
何故でしょうか?
理由は簡単です。
RTOSが使用するコードメモリとデータメモリの両方が、MSP430 Low-End Familyでは充分でなく使いたくても使えないからです。
FreeRTOSは潤沢なメモリーサイズを要求していません。むしろ、メモリー消費など良く検討され少ないデータメモリとコードサイズで多くの機能を持っている優秀なRTOSですが、さすがに
MSP430F2013 ROM 2KB RAM 128B
では、どうにもなりません!
TSPは下記の様なリソースがあれば、TSPがインプリメント出来ます。
コンパイル条件 ROMサイズ RAMサイズ スタック コメント CCS V4 924 bytes 20 bytes 20 bytes 以下 スタックはユーザープログラムのネスティングに最低限左記の値が必要です
ROMサイズの差は、最適化の違いより、アセンブラルーチンの違いが大きいmspgcc+Eclipse 904 bytes 20 bytes 20 bytes 以下
従って、MSP430F2013でもユーザープログラムと一緒に(1KB程度のユーザープログラムが実用になる分野は少ないでしょうが)使うことが出来るかもしれません。
<タスク制御>
項目 内容 補足 タスク
起動個数EDタスク(Event Driven Task)
-> 5 tasks
CYタスク(Cyclic Task)
-> 8 taskstsp_config.h内で使用するタスクの種類と個数を設定 TICK 周期 約2mSec(1.953125mSec)
あるいは約5mSec(5.00488mSec)32768kHz(Xtal)使用時
他のTICKも設定可能EDタスクの
起動マクロmReqed0(),mReqed1(),mReqed2(),mReqed3(),mReqed4()
の使用で、起動可能(起動最大遅れ時間はTICK周期分)割込みルーチンで最低限の処理を実施後、
左記マクロでEDタスクを起動するCYタスクの
起動周期起動周期は、あらかじめ固定された周期で起動可能
TICK周期の倍数でTick*255が最大周期となるtsp_config.h内で起動周期と最初に
起動するまでの時間を設定出来るタスクの
優先順位Mode0ではEDタスクがCYタスクより優先順位が高い
EDタスク内の優先順位は、最優先ED0、最下位ED4の固定
CYタスク内の優先順位は、最優先CY0、最下位CY7の固定
Mode1ではED0とED1は、優先順位は最下位となり
それらが実行中であっても、ED2,3,4あるいはCYタスクが実行されるtsp_config.h内にてUSE_MODE_0かUSE_MODE_1を選択可能
Mode0は、EDタスクは全て同じ条件で起動される
Mode1は、ED0とED1はプログラム中で事象を待つことが出来るタイマー機能 tim_usr0,tim_usr1,tim_usr2,tim_usr3の4つのタイマーが使用出来る
初期値を設定するとTSPがTICK毎にカウントダウンして
ゼロになった時点で減算停止タイマーは16bitなので、最大2分程度(Tick=2mS設定時)の
時間待ち可能
<CPU種類>
現在、下記のCPUで動作させたサンプルプログラムがあります。
基本的には、全てのMSP430シリーズで動作させることが出来ると思われます。
CPU種類 使用基板 動作条件 備考 MSP430F2013 eZ430/TI CCS4及びmspgccで
動作確認済無償版CCS v4で問題なし MSP430F169 MSP430-169LCD/Olimex CCS v4(無償版)では16KB以上の
大規模なユーザープログラムは動作せずMSP430F449 MSP430-449STK2/Olimex MSP430G2231 MSP-EXP430G2/TI 無償版CCS v4で問題なし MSP430FG4619 MSP-EXP430FG4618 CCS4での動作時には、
--core=430
にてコンパイルする必要あり
--core=430Xでコンパイルした場合には、
CALLA及びRETAでサブルーチンコールが行われ、
スタックの整合性が得られなくなる
--core=430でのコンパイル条件設定(CCS4使用時)
MSP430の最新チップでは、アドレス空間が64KBを越えるものが存在するようになって
スタックに20bitのアドレス情報をセーブする必要が生じ、CALLA/RETAが使用されるようになって来ています。
TSPは、現在CALLA/RETAに対応していませんが、近い将来には対応する必要がありそうです。
TPSの動作原理とファイル構成
<状態遷移図による動作説明>
全体の状態遷移は、このページのTOPにある図を参照願います。尚、下記説明はMode1を前提としています。
状態 説明図 動作説明 補足説明 パワーオン
リセット後の
動作(下記LPM3の状態になる前の状況です) リセット後は通常動作としてCのmain()ルーチンに辿りつきます。
これは、ユーザープログラムとしてタスク記述を行うtasks.cの中の
最下部付近にあります
下記がそのルーチンです
int main( void ){ /********** Hardware *********/ mStop_WDT(); mSet_SysClock(); // Ports (see "hw_config.h") mInit_P1_as_PORT(); mInit_P2_as_PORT(); mADC_init(); /********** TSP **************/ tsp_msp430_init(); /** Task related data routine **/ task_data_init(); /** Interrupt ****************/ __enable_interrupt(); /** Main loop ****************/ goto_tsp_infinitloop(); /*** Dummy return ************/ return 0; } void base_loop (void){ asm(" incd.w r1"); // Save 2byte while(1){ LPM3; // Enter LPM3, Only ACLK is running _NOP(); } }
左記、 tsp_msp430_init()内
で、TPSの初期設定を実施し、
task_data_init()内で各タスク
の初期化を実行しています
左記、goto_tsp_infinitloop()
(tsp_msp430.c内)に制御が
移行するが、すぐに、
base_loop()に制御が移行し
LPM3のモードに入ります通常動作時の
タスク起動なし上記、LPM3で低電力モードに入っているとACLKで動作させている
TimerA(WDTでも可能)の割込みが発生する(TSP用TICK割込み)
LPM3→1→Timer_A()
割込み処理内では、tsp_core()がサブルーチンコールされる
Timer_A()→2→tsp_core()
tsp_core()内では、EDタスクの起動要求の有無をチェックし、CYタスク
の起動周期に達したかチェックし、それらのいずれでもない場合には
再びbase_loop()内の無限ループ内も戻る
tsp_core()→3→LPM3
この一連のループが、Tick周期(2mSもしくは5mS)毎に発生
しタスク起動に備える 本来、RTOSでタスクの起動周期を
正確にする為には、Tick周期を
早めることが重要だが、MSP430の
低消費モードとのトレードオフとなる
今回は、起動周期を犠牲にして.
低消費電力を優先している. タスク起動
(例としED1)tsp_core()が起動される前に、mReqed1()のマクロが、ユーザー作成
の割込み処理プログラムもしくはCYタスク内で実行されたいた場合に
は、tsp_core()から、tsk_dispatch_lo( &ed1_main )のサブルーチンが
呼び出される
tsp_core()→4→tsk_dispatch_lo()
tsk_dispatch_lo()内では、ED1へのサブルーチンコールをす
る以前にスタック内に必要な情報を入れて整合性を保つ準備
をする
その後、RETIにてED1へ制御を移す
tsk_dispatch_lo()→5-2→ED1
ED2からED4、CY0からCY7のタスク
起動では、tsk_dispatch_hiが呼び出
され、その後左記と同様に各タスク
へ制御が移行するタスク終了
(例として
ED1からの
復帰)タスクは、基本的にはCのサブルーチンとして記述される
更に、各タスクは事象を待つ様なプログラムや無限ループの処理は
記述できない
なるべく早く、処理を終わりにするような記述方法が求められる
但し、Mode1の設定をしたTSPではED0とED1の2つのタスクに限って
事象待ち記述が許される
低消費電力を維持するためには、なるべく早くLPM3へ戻す必要が
ある
ED1のサブルーチンがRETで終了すると、あらかじめスタック内に戻り
先として、task_endの先頭番地が入っているので、処理が移行する
ED1→6→task_end()ED0もしくはED1が起動されている
状態で、TimerAの割込みが発生し
tsp_coreがその他のタスク起動が
必要と判断した場合には、そのよう
に起動する
ED1実行中→1→TimerA()→2
→tsp_core()→4→task_dispatch_hi()
→5→(たとえば)CY0→6→
task_end()→7→ED1で再び継続タスク終了
(続き)task_end()に制御が戻ると、最初に割込みで処理が中断している戻り
番地に制御を移すためにスタックを整理して割込みからの戻りRETI
で復帰する.
<ファイル構成>
ディレクトリ ファイル名 主な機能 備考 msp430_TSP_v2 tsp_msp430.c TSPコア機能を記述したCソースファイル Inlineアッセンブラはmspgccのみに使用 tsp_msp430.h TSP構成記述のヘッダーファイル TSP利用時には、ユーザープログラムのファイルにも入れること tsp_tsk_dp.s TSPのスタック操作を記述したアセンブラファイル CCS V4使用時には必須。mspgccでは実際には使用されない 1st_init.c WDTをmain()前に停止する処理ルーチン 他にも記述可能 Root Directory tasks.c TSPのユーザープログラムを記述する main()などTSPの一部の機能を含む tsp_config.h どのタスクをどのように使いかを決定する ここでタスクを使用しないと、コンパイル時に不要コードが削除される hw_config.h MSP430のCPU選択 ハードウェア関連処理のマクロ記述 isr4debug.c 割込みベクター記述 ユーザー処理が無い割込みは無限ループでDebug対応している
<file name> TSPに必須のファイル <file name> TSPに直接関係しないがプログラム実行に必要
TPSの使い方
<ユーザープログラムの作り方>
ユーザープログラムは、下記のように記述します。
---- tasks.c ---- void cy0_init( void ){ mP10_OFF(); } void cy0_main( void ){ // Only debug purpose ***START*** mP10_TGL(); // Toggle the P1.0 output // Only debug purpose ****END**** } ---- tsp_config.h ---- // CY0 #define USE_CY0 ACTIVE #define CY0_P T_250MS #define CY0_S (T_250MS+T_50MS) #endif ---- hw_config.h ---- #define mP10_ON() (P1OUT &= ~(1UL << 0UL)) #define mP10_OFF() (P1OUT |= (1UL << 0UL)) #define mP10_TGL() (P1OUT ^= (1UL << 0UL))
CY0のタスクは、イニシャライズ時に一度だけ呼ばれるcy0_init()と周期的に呼ばれるタスクcy0_main()とで成り立ちます。
両方ともCの関数として表現し、引数も戻り値もないvoid型とします。
cy0_main()の呼出し周期はCY0_Pとして、tsp_config.h内で定義され、上記の例では250mS毎にポート1.0の出力が反転します。
CY0_Sは、cy0_init()の呼出しタイミングを設定します。この値は、一度だけしか呼ばれませんが、他のタスクとの順番や間隔を決める重要な値となります。
EDタスクは、下記の様な使い方が出来ます。
---- tasks.c ---- void ed4_init(void){;} void ed4_main(void){ adc_data = ADC10MEM; // Saves measured data //この部分にADCの正規化ルーチンを記述 } /**** ADC Interrupt routine ****/ #pragma vector=ADC10_VECTOR interrupt void ADC10_isr(void){ mReqed4(); // Request to start ED4 task LPM4_EXIT; // Clear LPMx bits from 0(SR) }
この例では、ADCの終了割込みでED4のタスクed4_main()に制御を引き継ぎます。
正規化ルーチンをED4タスク内で処理するようにして、割込みルーチンを短時間で終了させることが出来ます。
上記二つの例は、ポートの出力初期化やADCの初期設定が記述されていません。
これらは、各タスクの初期化前にtasks.c内のmain()関数の中でhardware_init()を呼び出すことによって行っています。
<注意点>
一番重要な注意点は、
事象を待つループを作らないこと
です。
---- tasks.c ---- void ed4_main(void){ // BAD Sample while (( (P1IN & 0x10) == 1 ){ ; } mP10_TGL(); }
上記の記述では、ポート1.4の端子の状態によってプログラムが終了しないで長い時間ループ内で待つ可能性があります。
TSPはタスクを強制的に切り替える機能を持っていません。
必ずユーザープログラムが他のタスクに「影響を与えない程度の時間」で処理を終了する必要があります。
この「影響を与えない程度の時間」は、一概には定義出来ません。
ユーザープログラムの機能やI/Oに何が接続されているかに依存されます。
IF文やSWITCH文などで条件分岐して処理出来るプログラムで全てが記述出来れば問題ありませんが、
世の中にはある条件が整ってから次のステップに進み、更に別の事象を待って更に次の処理を・・・
というようなシーケンシャル処理が存在します。
このような処理は、状態遷移図で処理概念をまとめてから各ステートを一定期間毎にチェックしてシーケンシャルな動作をさせるプログラム構成とします。
下記のプログラムは、モールス信号をLED表示で実現するプログラムの一部です。
switch文を使って、各ステート毎の処理を実施していることが判ると思います。
---- tasks.c ---- void cy2_main( void ){ switch ( morse_status ){ // Check display status case IDLE:{ // Idle condition if (tim_usr0 == 0){ switch ( ++morse_disp_data ){ case D_NONE:{ tim_usr0 = T_10S_16B; // Wait 5 sec. morse_status = IDLE; // Stay idle again break; } case D_VCC:{ normalize_adcvcc(); int2bcd4morse( nor_adc_vcc ); morse_status = HEAD; break; } case D_TEMP:{ normalize_adctemp(); int2bcd4morse( nor_adc_temp ); morse_status = HEAD; break; } default:{ morse_disp_data = D_NONE; } } } break; } case HEAD:{ // Head condition if ( --morse_step ){ // Make START part of LED on mLED_ON(); // LED on } else { // Finish START part mLED_OFF(); // LED off morse_status = ONGOING; morse_sb_stp =4; // Set 1st data morse_disp_no = (unsigned char)morse_data.bt.bhh; } break; } case ONGOING:{ // Display code if ( morse_step == 0 ){ // start the action morse_step = 0xff; // get disp code morse_stp = morse_num[morse_disp_no]; } else if ( morse_stp ){// ongoing // Check On/Off state if ( morse_onoff[morse_disp_no] & morse_stp ){ mLED_ON(); // LED on } else { mLED_OFF(); // LED off } // Preparation for next digit morse_stp = morse_stp/2; } else { // end the action morse_step = CLS_OFF; morse_status = CLOSE; } break; } case CLOSE:{ // Termination / long blank if ( --morse_step == 0 ){ if (--morse_sb_stp == 0){ morse_status = IDLE; morse_step = ST_ON; } else { switch (morse_sb_stp){ case 3:{ morse_status = ONGOING; // Set 2nd data morse_disp_no = (unsigned char)morse_data.bt.bhl; break; } case 2:{ morse_status = ONGOING; // Set 3rd data morse_disp_no = (unsigned char)morse_data.bt.blh; break; } case 1:{ morse_status = ONGOING; // Set 4th data morse_disp_no = (unsigned char)morse_data.bt.bll; break; } default:{ morse_status = IDLE; morse_step = ST_ON; tim_usr0 = T_5S_16B; // Wait 5 sec. } } } } break; } default:{ // Just in case morse_status = IDLE; morse_step = ST_ON; } } }
------- 各Cソースファイルでの記述例 ----- #ifndef __TI_COMPILER_VERSION__ #include <signal.h> #include <io.h> #endif
#ifdef __TI_COMPILER_VERSION__ #pragma vector=DACDMA_VECTOR interrupt void DACDMA_isr(void){for(;;);} #else wakeup interrupt (DACDMA_VECTOR) DACDMA_isr(void){for(;;);} #endif---- tsp_tsk_dp.s内での記述例 ------ ; IMPOTANT! ; if you use TI CCS, "TICCS .equ 1" ; if you use MSPGCC, nothing ; .if .MSP430 == 1 TICCS .equ 1 .endif
”__TI_COMPILER_VERSION__”は、CCSのVersion値を返すMacro定義ですが、これはmspgccでは未定義なので上記のように使用することで2つのコンパイラを自動識別出来ます。
同様にアッセンブラルーチンでは、”.MSP430”が使用出来る事が判りました。
プログラム作成例
サンプルプログラムをここでダウンロード出来ます。