タスク概念を既に理解している技術者を対象に記述する
超簡易タスク
概要
256 bytes のラムしかない One chip uCom でも使用可能な、ラムの消費を最小に押さえた、コンパクト・単純なオリジナルの RTOS について述べる。
------ 主要緒言 ------
簡易 RTOS を記述する ?? 500 line
C++ source の行数
TCB の大きさ 6 bytes/task typically
RTOS のメンバー関数 protected:
int wait(int inDelay, in inSusped)
void endTask()
void resume(int enRestart);
void startTask()
public:
void End()
RTOS を単純化するために、
- C 言語から利用することを前提とした。RTOS 自体もできるだけ C で記述した。アッセンブラの使用をできるだけ避けた。
- 割り込みルーチンの中からのタスク制御を禁止した。
- ノン・プリエンプティブな RTOS とした。タスクの優先順位は 1 レベルだけとした。
- セマフォ、メッセージなどの同期機構をやめ、ディレー・中断機能を持つ wait(.) によってタスクを制御する
ようにした。このように単純化することでプログラマの記述が制限されてくる。しかし次の対策をしたので、ワンチップ・マイコンのような小さな規模では、記述の制限は問題とならない。
- タスクに付属するポーリング関数を用意した。この中でタスクの開始、再開を制御することで、割り込みルーチンの中からのタスク制御禁止の代用とする。ポーリングは次の望ましい性質を持つ。
- 割り込みルーチンの中で変更するデータは、Test and Set の狭間に生まれるボラタイルなデータとなってしまうデータであり。このデータを割り込みルーチン以外から読み書きするとき注意が必要となる。
- ポーリング・ルーチの中で変更するデータはボラタイルにはならない。
- ポーリングは割り込みより遅いが、タスクよりは早く処理できる。
- タスクの優先順位を多重にする代わりに、次に示す順序に応じて優先する処理を割り振る。
- 一レベルのノン・プリエンプティブな RTOS なので、セマフォなどの同期機能はユーザーが簡単に自分で記述できる。ボラタイルなデータが発生するのは、割り込みだけに限定されるからである。
逆説的ではあるが、RTOS を上のように 単純化することで、今までにない、有利な面も出てくる。
- この RTOS は大部分を C で記述しました。CPU 依存の部分を最小にしてあります。コンテキスト・スイッチに関連するコードの数十行を除いて、CPU の種類が何であっても共通して有効です。ワンチップ・マイコンに限りません。 MS Windows:x86 でさえも、動作させることが可能です。
- この RTOS が x86 でも動作するので、アプリケーション部分を C で記述してやると、そのプログラム全体を MS Windows などのクロス・プラットフォーム上で C ソースのレベルで実行できるようになります。。マシン言語をシミュレーョンすることなく、C の段階のままで実行・デバッグ・確認ができてしまいます。「シミュレータ」ソフトを導入すれば、自動デバッグも可能になります。。プロファイラーのカバレッジを使えば、デバッグ進捗度を客観的な % 数値で表示することもできます。シミュレータについては稿を改めて、「シミュレータ」の稿で論じます。
- この RTOS は割り込み禁止期間を設けない。アプリケーションの高速応答を制限しない。割り込みをユーザーが自由に使える。
- タスク・スタックを共用できるようになる。これによりラムの消費を大きく下げることができる。スタックの共用は、タスク・コンテキストの前後でオート変数の値が破壊される弊害を持つ。それでも、ラムの消費を下げる価値は有る。ラムに余裕があるときは、通常のタスク毎にスタックを使用する TCB を選択する。
- スタックを共用する時は、TCB に保存するデータ量を少なくする。通常は 6 bytes/task にできる。これはコンテキスト・スイッチの高速化にもなる。
- タスク再開条件の判定コードをタスクの中のコードに埋め込むことが可能になる。タスクとは別の関数に分けることなく、直接タスクの中に判定コードを記述できる。このタスク途中に記述した判定コードブロックを、外部 polling から呼び出すようにコード・ブロックを定義できる。タスクの途中に while(1){....break;..} に似た DfDoLoop ..Dfbreak... DfLoopEnd といった記述を埋め込むことが可能になる。再開条件を判定するポーリング処理を、タスクのプログラム・コードの中に埋めることが可能になった。別の関数に分けずにすむので、プログラムの見通しがよくなった。それでも単純なコンテキスト・スイッチのため CPU の時間負荷の増大を少なくできている。
- ノン・プリエンプティブにすることで TCB に残っている戻りアドレスの値が、タスクがディレー・中断したあとの CPU の占有を終わったあとのアドレス値である保証ができた。これにより、タスクが特定の状態にあることを context に保存してある戻りアドレスの値の範囲に入っていることで判定できるようになる。もちろん、タスクが特定の状態にあることをこのように判定するためには、特定のコードを一つの固まりとして、まとめて記述して、プログラムのコードを配置せねばならない。
- TCB の戻りアドレスの値が特定の範囲に入っていることを利用して、タスクが特定のモードにあることを判定できるようになった。フラグなどの変数を使わずに済むようになった。これはプログラムのバグを大きく少なくする。
- RTOS が単純になった。コメントも含め 1000 行以下である。ユーザーが余裕を持って追跡できるソース・サイズである。ブラック・ボックスではない、完全にホワイトな RTOS である。大袈裟ではなく、RTOS の説明のほうが C のソースより分量が多い。説明を読むより、C のソースを読んだほうが理解に要する時間は短くてすむ。。
タスク毎にスタックを設けないと、コンテキスト・スイッチを含むタスク・サブルーチンを記述できなくなってしまう。単純なシーケンシャル処理では、タスク・サブルーチンを使わずに済むことも多い。しかし、少し複雑なタスクを記述するようになると、タスク・サブルーチンが必要になる。はラムの消費を少なくするためであっても、タスク・サブルーチンを使えないことは、プログラムの記述としては制限がきつすぎる。このために、タスクの戻りアドレスなど、どうしてもタスクが保存しなければならない変数に限ったタスク・スタックを用意する。それでも、大部分のオート変数や、コンテキスト・スイッチを含まないサブルーチンの戻りアドレスは共通スタックを使うようにする。このため、タスク・スタックのサイズは、コンテキスト・スイッチを含むタスク・サブルーチンのネスティングの数程度に限定できる。通常は 10 byte もあれば良い。タスク・サブルーチンの呼び出しには、専用の関数を介在させることになる。callTaskSub( inTaskStack, fnTaskSub,...)の形式である。こうすることで、タスクの記述を制限すること無く、ラムの消費を少なくした、ワンチップ・マイコンでも使用できる、シンプルな、でも十分に実用的な RTOS を実現できる。
なおサンプル・コードも含め C++ で記述してある。 STL も使っている。実際のワンチップ・マイコンでは、まだ C++ を使えないことは分かっている。以下の理由で C++ を使う。
- C よりも記述を簡単にでき、見通しが良くなる。この RTOS は元々 C で記述してあった。しかし、説明を単純にするため C++/STL を使う。
- 元の RTOS は C で記述の中に OOP 的な考えを取り入れている。このために、余分なマクロを使っている。そのぶんだけ RTOS が複雑になっている。C ソースは説明に適さない。
- ワンチップでの RTOS ソースには、TCB データの一部分を ROM 変数に割り当てるコードも入ってくる。RAM の消費を最小にするためである。これも、RTOS のコードを複雑にする。C ソースは説明に適さない。
- Cl...TCB を継承して is a 関係となるタスクを作るかわりに has a 関係のように見なしタスクのコードを、C では必要になる。これも、コードを複雑にしてくれる。
- STL のリストではなく、C の範囲でのリストを記述することも複雑さを増す。
- コンテキストの切り替えの記述は、個々の CPU, コンパイラに合わせるべきである。これは汎用的な記述になじまない。ここでは X86. VC5.0 に合わせたコンテキスト切り替えのみを記述する。
- 逆に、この RTOS を理解していれば、C のコードに置き換えていく作業は簡単である。
- この RTOS を使って記述したコードは、パソコン上で、実際に動作する。後に VC5.0 のデバッガ上で動作する実際のコードを示す。
- auto_ptr を使うことで new で生成したタスクの消滅を保証できる
これらは VC5.0 Service Pack3 でコンパイルし、また実際に動作することを確認してある。
PrimitiveTCB 目次
簡易 RTOS を開発した理由 ---------------- page
簡易 RTOS の適用範囲 ---------------- page
main ループ、RTOS の機能と実行 ---------------- page
ボラタイルなデータ---------------- page
ノン・プリエンプティブとする理由---------------- page
ポーリング関数の導入---------------- page
割り込み、ポーリング、タスクによって処理の優先度を分ける----------- page
??セマフォ機能をユーザーが作る---------------- page
??ポーリング関数を持つタスク ---------------- page
PrimitiveTCB と SimpleTCB (TCB 構造) ---------------- page
??main loop と PrimitiveTCB の関係 ---------------- page
SimpleTCB TASk STACK の共用
RAM の消費を少なくするためにスタックを共用する ---------------- page
メイン・スタックとタスク・スタックを共用できる理由 -------- page
従来のタスク・スタックを共用しない TCB ---------------- page
タスク・スタックを共用することによる問題点 ---------------- page
コンテキスト・スイッチを含むタスクサブルーチンの使用 ------ page
コンテキスト・データをより IP だけにしたときの問題 ------ page
TCB に保存してある戻りアドレスを利用した、釦状態の解析プログラム --- page
どのように使うか ---------------- page
セマフォ・メッセージなどの同期機構を省略できる理由 ---------------- page
タスク・スタックを共用する手段 ---------------- page
タスク・サブルーチンを必要としない大規模な処理の例(釦・表示処理タスク) ---------------- page
専用のTASK STACK
RTOS の C++ ソース ---------------- page
C 言語での実装に変更する ---------------- page
C++ と C ソースの違い ---------------- page
単純化できる理由
冷蔵庫の制御・AV 装置の制御となどの
- ラムの少ない
- ノン・プリエンプティブな
- wait(.)( delay と suspend ) のみによるタスク制御
マイコンに RTOS の対象を限定するからである。
実際にワンチップ・マイコンのプログラムを書いてみれば、多くのタスク記述はディレーとサスペンドだけで済んでしまう。Windows のように GUI を制御するプリエンプティブなマルチ・スレッド OS までは必要としない。セマフォなどの機能がなくても、たいして困らない。ディレーとサスペンド程度ならば、C で記述した RTOS は 500 -- 1000 行程度で済んでしまう。これでは、RTOS としてビジネスにすることは難しいだろう。
私としては、別に論ずるデバッグ・シミュレータと組み合わせることでビジネスにしたいと考えている。
タスクごとにポーリング・ルーチンを設ける。
多くの RTOS が備えるに semaphor や event などのタスクの同期制御機能は設けていない。
- 割り込みルーチンの中からタスク制御ルーチンを呼び出すことを禁止する
- ノン・プリエンプティブな RTOS とする
- タスクの優先度レペルを一重だけと単純化する。
RAM が 512Byte, 1KBytes と極端に少ない one chip uCom 用の極端に単純化した real time monitor をの機能に付いて論ずる。
単純化のために
- Non Preemptive とする
- タスクの優先順位は 1 レベルの固定優先順位とする
- 割り込みルーチンの中からのタスク制御は禁止する。モニタ導入による割り込み応答遅れは発生しない。
- タスクの制御機能としては以下のものだけを用意する
- タスクの生成消滅
- タスクの wait ... delay と suspend の両方の機能を持たせる
- タスクの開始と終了、および、タスクの強制終了
単純化を優先する。有名な Semaphor や message などの同期処理は省略する。ノン・プリエンプティブであるため、セマフォが必要になったら、簡単にそれを自分で記述できる。ワンチップ・マイコンでは、ディレーとサスペンドがあれば、タスク処理を記述できてしまう。これは実際に記述してみれば体感できる。
割り込みルーチンからのタスク制御を禁止する変わりに、はユーザーにも polling 処理を許す。積極的に、タスクごとに polling() 関数を設定できるようにする。割り込みルーチンからのタスク制御がどうしても必要となったときは、割り込みルーチンと polling routine の間でフラグなどの変数をやり取りする。そしと polling routine の側でタスクの制御を行う。(link)
class ClPrimitiveTCB{
public:
// タスク状態を示す。
enum EnTcbState{ EnTimeup, EnDormant, EnStarted, EnRun ,EnOnlyDelay, EnSuspend};
protected:
virtual void startTask()=0; // タスクを開始させる。
virtual void endTask()=0; // タスクを終了させる。
// ディレーとサスペンドの機能を併せ持つ
virtual int wait(TyWord wdDelay = 0, int enSuspendAg = EnOnlyDelay)=0;
// 待ち状態にあるタスクを Reday 状態にして再開させる。
virtual void restart(int enRestartAg)=0;
virtual DfVoid polling();
・
・
protected:
virtual void task(){} // ClPrimitiveTCB を継承して実際のタスクを記述する
static list< ClPrimitiveTCB* > m_theLstTCB;
public: // main loop から呼び出す TCB グループへの interface 関数
static void InitializeTCB();
static void PollingTCBList();
static void TickTCBList();
static void ExecuteReadyTCBList();
};
(link) ClPrimitiveTCB の実際のコード
メイン・ループと
- タスクの生成消滅は m_theLstTCB リストに接続するか、接続を外すかで実装する。C++ を使う時は、constructor, destructor のなかに、この処理を記述でき、単純になる。C で記述するときは、リストにつないだり、外したりする操作を明示的に記述せねばならない。
- 最初に一度だけ初期化する: InitializeTCB()
- メインループの初めに: PollingTCBList()
- ディレーの最小間隔時間が経過していたらディレー値の減算処理をするTickTCBList()
- 開始を指示されたタスク、またはタイム・アップ、リスタートによってレディー状態になったタスクを探し、それを実行する
次のように接続する。
void main()
{
extern void test();
ClTestTask clTaskAt;
clTaskAt.Start();
ClPrimitiveTCB::InitializeTCB();
setjmp(Main_JumpBuffer);
for (;;){
extern IsTickInterrupt();
ClPrimitiveTCB::PollingTCBList();
if ( IsTickInterrupt() ){
ClPrimitiveTCB::TickTCBList();
}
ClPrimitiveTCB::ExecuteReadyTCBList();
}
}
task 1 task 2 ----------
┌──────────┐┌──────────┐
初期化 │interrupt(){...} ││ │
loop │ ││ │
│個々のタスクに備えた │ClTask1::polling() ││ClTask2::polling() │
│polling 関数の呼び出し │{...startTask();..} ││{...restart();..} │
│ │ ││ │
│ディレー値の減算処理 │ ││ │
│ │ ││ │
│レディ状態のタスクを │void ClTask1::task()││void ClTask2::task()│
│実行する │{ ││{ │
│ │ ・ ││ ・ │
│ │ wait(300); ││ wait(50,Suspend)│
│ │ ・ ││ ・ │
│ │} ││} │
loopend └──────────┘└──────────┘
逆説的だが、機能の少なさは、新たな機能をもたらしてもくれる。とくにプリエンプティブにしていないこと、割り込みからのタスク制御を禁止したことを積極てきに活用している。すなわち、タスクのポーリングや、タスクの中から別のタスクの状態を調べる時、そのタスクは停止しているかウェートしているかのどちらかである。ラン状態でないことがほしょうできる。
- タスク・スタックの共用が可能になる
- タスクのコードの一部を polling ルーチンとできる。suspend 状態なったとき、リスタート判定の polling 処理を別関数にすることなく、タスクのなかに直接 Do loop として記述できる。
- 戻りアドレスが特定の範囲に入っていることを利用して、特定のモードにあることの判定に使える。フラグを設定すること無く、モード判定が可能となる。10 数個の多重に機能する行の釦・表示プログラムで、 2000 行の規模のプログラムでフラグを全く使わずに記述することができる。
他の RTOS ではみられない便利な機能として、サスペンド処理のインライン記述を可能にした。 通常のプログラムの途中で、条件判断(ここでは isSwichOn() )の polling 待ち機能を入れたい時
・
・
┌───────┐
│ Switch が ON │
│ するまで待つ │
└───────┘
・
・
の処理をしたいとき
DfDoLoopT(inTimeupValue)
・
・
if ( isSwitchOn() ){
DfBreak;
}
DfLoopEnd
のように記述する。DfDoLoop(.).... DfLoopEnd の間を polling によって何度も呼び出す。
タスク・スタックの共用
また単純化のためというより、ラムの消費を少なくするために、複数のタスクや他のルーチンでスタックを共用する。全体で一つのスタックだけですます。
ワンチップ・マイコンでタスクを導入することを難しくしているのが、ラムの少なさである。タスク毎に 100 bytes 程度のスタックを用意すると、全体で 500 byte しかないワンチップマイコンでは、アプリケーションで必要なラムが足りなくなってしまう。
このために、タスク・スタックを共用する。タスク専用のスタックを設けない。もともと、C のオート変数は、スタック・フレームに対するオフセットとして扱われているので、これが可能になる。もちろん代償も大きい。コンテキスト・スイッチの発生に伴いオート変数の内容が壊れてしてしまう。
int inAt;
・
・
inAt = 0x55;
if ( inAt == 0x55 ){ // 問題ない
・
・
}
inAt = 0x55;
wait(wdDealy); // コンテキスト・スイッチの発生
if ( inAt == 0x55 ){
// コンテキストスイッチが発生したので、オート変数 inAt は壊れる。
・
・
}
inAt = 0x33;
if ( inAt == 0x33 ){ // 問題ない
・
・
}
コンテキスト・スイッチが間に入らければ問題ない。コンテキスト・スイッチに伴い、C 言語の auto 変数が破壊されるので、プログラマーは、これを避けるようにプログラムを記述せねばならない。タスクの記述に余分な注意が必要になる。しかし、余分な注意を払ってでもラムの消費を少なくするほうがましである。それだけ、ワンチップ・マイコンではラムが゜少ないからである。
スタックを共用することで、コンテキストに全レジスタ値を保持する必要もなくなる。TCB に必要なラムも 4 または 6 byte/task と小さくできる。(compiler によって変化する。)
タスク専用のスタックを設けないと、コンテキスト・スイッチを含んだサブルーチンが記述できなくなる。スタックに記録した戻りアドレスが壊れてしまうからである。
void taskSubroutine()
{
・
// コンテキスト・スイッチから戻ってきた後
// taskSubroutine(.) からの戻りアドレスのデータが壊れている。
wait(wdDelay);
・
}
void task()
{
・
・
taskSubroutine();
// taskSubroutine() の中でコンテキストスイッチが発生したので
// ここに戻ってこれない。
・
・
}
これは、プログラマーが注意して対処することで済まされる問題ではない。次のように対策する。
- オート変数のスタックとは別にタスク・サブルーチン専用のスタック・データ(inArStack)を設ける。
- タスク呼び出し CallTaskSub(.)と、タスクからの戻り(ReturnTaskSub(.)を、タスク・サブルーチン専用のスタックを使って行う。
static inArStatc[3]; // nesting level に応じて変わる。
void taskSubroutine()
{
・
// コンテキスト・スイッチから戻ってきた後
// taskSubroutine(.) からの戻りアドレスのデータが壊れている。
wait(wdDelay);
・
// inArStack の中の戻り番地のデータは保持されている。
ReturnTaskSub(inArStack);
}
void task()
{
・
・
// inArStack のに戻り番地のデータを保持させる。
CallTaskSub(inArStack, taskSubroutine, ...);
// taskSubroutine() の中でコンテキストスイッチが発生しても
// こに戻ってこれる。
・
・
}
タスク・サブルーチン専用のスタック・データのサイズはは、タスク・サブルーチンのネスティング回数で決まる。通常は数回ですむはずだ。専用のスタック・データを設けることで増えるラムのバイト数は必要最小限に押さえられる。CallTaskSub(.) などを使うことは、プログラマーに余分な負担をかけるが、ラムの確保で悩まされるよりはましである。
単純な処理のタスクではタスク・サブルーチンを使わずに済むことも多い。リモコン受信信号の二回一致を確認する処理を一例としてあげる。押された釦の順序や押しつづけた時間によって動作を変えるキー解析処理もそうである。(キー解析をタスク処理にすることで、モードによって釦の動作が変わることを単純に記述できようになるメリットも有る。)(link)
もちろん、通常のタスク毎にスタックを用意する RTOS に戻すことも簡単にできる。RTOS の記述としては簡単になる。C++ の記述としては継承した後に、タスク・スタック・データ・メンバーを追加し、wait(.), restart(.) 関数をオーバー・ロードしてやるだけである。しかし、意味が少ないと思われるので、タスク毎にスタックを持つ時は、以後は論じない。
RTOS の全体ブロック図
┌────────┐m_theList ┌───────────┐
│ClUltraSimpleTCB├────────┤list<ClUltraSimpleTCB>│
│ │ └───────────┘
├────────┤
│enum EnRestart │
│wait(int inDelay│
│ ,EnSuspend ag)│
└───┬┬┬──┘
△
│
┌────────┐
│ClSimpleTCBWithStakc
│ │
├────────┤
│int m_inArStack │
│CallTaskSub(.) │
│ReturnTaskSub(.)│
└───┬┬┬──┘
タスク相互の同期機能を省略できる理由
全体の RAM が 0.5 -- 2KBytes のモニターと、32KBytes のモニターでは、モニターの機能が変わるべきである。共用することに無理がある。TCB が保持すべきデータ量の段階で、違いが出てくる。タスク・スタックとレジスタ待避領域だけで 100 bytes 程度は必要となる。アプリケーションが使うラム領域を考えると、1K bytes のラムしかない one chip uCom では 2 -- 5 本程度のタスクしか並列に走らせることができなくなる。タスク分割に無理が来る。
1K bytes 程度の RAM しかない one chip uCom ではタスク制御機能を削ってでも、タスクの記述に無理が発生しても RAM の消費を押さえるべきである。64K 程度の ROM サイズの複雑さでは 5--10 本程度の並列に走るタスク処理が発生することが普通と思われるからである。
- タスクの優先順位は一レベルの固定優先順位のみとする。マルチ・レベルの優先順位は設けない。
- round robin にすることも容易である。しかし key 解析などの優先順位を意識的に下げるタスクがあるので、固定優先順位とする。
- タスクに詳しい設計者は key 解析をタスクにすることに違和感を感じるかもしれない。これについては別途詳細に論ずる。(link)
- Non preemptive とする。
- task の start, terminate 及び delay と suspend 機能のみを real time monitor の task 制御機能とする。
- semaphor, event, message などのタスク同期制御を設けない。
- その代償として、タスクごとに polling 関数を設ける。
- task の性質は ClTask class に実装する
- delay, suspend などの性質を必要とするアプリケーション・タスクは ClTask を継承する。
- task の生成・消滅は ClTask の constructor, destructor が制御する。
- 割り込みルーチンからのタスク制御は行わない
- 必要ならば、割り込みルーチンでフラグを設定する。 tas 毎に設ける polling routine でフラグを参照してタスクを制御する。
- ??uSec の速度でタスク・スイッチを行っても、その価値は少ない。
- real time monitor は一切の割込み禁止タイミングを設けない。
- main routine も user が設計する。モニタは必要なスケルトンを示すのみである。
- コメントも含めて C++ で 300 行の real time monitor である。
- user が全部のソースを一行ずつ追うことが可能である。Black box にならない。
並列に動作するタスクの間でスタックの共用を可能とする。task subroutine などのどうしても省略できないタスク・スタックのために、小さい、特別なタスク・スタックを用意する。
Non preemptive であることを積極的に利用している。
このタスクを論じた後、このモニタ上で動作させる simulator, tester soft について論ずる。
タスクにノンプリエンプティブにして優先順位を設けない理由
通常は 1--2 人、多くても数人の programmer でしか作らない、64KByte 以下のサイズの one chip uCom ではタスクにマルチ・レベルの優先順位を設ける必要がない。
- タスクの本数は多くても 10 数本までである。 10 以下になることが多い。20 以上のタスクを並列に走らせたくても、そのとき必要となるラムが存在しない。
- 割り込みルーチン・polling ルーチンといった、高速に処理する機能が既にある。
多重割り込みは、できるだけ避けるべきである。Test and Set の動作タイミングの狭間でトラブルを発生させるからである。同様な理由で、multi level の preemptive なタスク切り替えも、避けるべきである。64KByte 程度の小規模なアプリケーションならば、割り込みと polling だけで十分なはずである。
大規模であるがタスク・サブルーチンを必要としない、タスク・スタックを共用できる例として「釦・表示処理タスク」をあげる。ここで、タスク・サブルーチンとは、タスク・コンテキスト・スイッチを含むサブルーチンである。タスク・コンテキスト・スイッチを含まない通常のサブルーチンは普通にしよできる。ワン・チップ・マイコンが扱うことの多い家電機器では 10 数個の釦と、操作結果をユーザーに知らせる表示装置を制御することが多い。この部分はユーザー・インターフェースをつかさどる部分でもあり、プログラムの規模も大きくなり易い。数千行の規模になることは珍しくない。CD プレーヤの再生順序のプログラム設定や VTR の予約操作を思いうかべてほしい。その複雑さが解ると思う。
この釦・表示処理をタスクとして記述することで、
- 釦を一定時間以上、押し続けたときの動作
- 点滅表示などの表示のシーケンシャルな変化
をタスクを使って記述できる。さらに、タスクを使うことで、キーの意味で動作順序によって異なる、動作モードによってキーの働きが異なる状態を上手く記述できる。すなわち、動作モードをタスク・コンテキストに対応させることで、モード変数や、モード・フラグの代わりに、タスク・コンテキストを含んだサブルーチンを呼び出す。
こうすることで
- 釦・表示の動作仕様とプログラム・コードの対応が良い、すなわち、人間の認識に近いプログラム・モデルとしてコードを記述できる。
- タスク・コンテキストを使うことで、モード変数、モード・フラグをなくしてしまう。
この結果、モード状態を判定する条件判断も省略できるようになる。効果がある。仮想関数は使わないが、これらは OOP プログラム導入の目的でもあるはずである。タスクを使ったプログラムの構成が、人間の対象認識・モデル化に似ているため、このようになると思う。
ここでは CD プレーヤの早送り ()と、次曲のサーチを兼用する >>/>>| 釦を例として、釦と表示処理を、動作に即した見通しの良いタスク記述としてみせる。次の動作仕様をプログラムのコードとして表現する。
- >>/>>| 釦を押したときは反応しない
- 一秒以上押し続けたときは、早送りを行う。
- 釦が押している間早送りを続け、釦を押すことを止めたときプレーに移る。
- 早送りの最中は、早送り LED を 0.5 秒間隔で点滅させる
- 一秒以内で釦を離したときは、次曲のサーチを行う
C 言語での実装に変更するとき
- polling メンバー関数のために const 構造体変数の導入
- wait を delay と suspend に分割する
- m_blReady, m_enState, m_delay を 16bit size の bit field に割り振って、ラムの消費を少なくする。
- context switch の記述をアッセンブラ記述にして、高速化をはかる。
ITRON への不満
64K byte ROM 1K byte RAM 程度のワンチップ・マイコンには重過ぎる。また古い概念に縛られている。TCB に関連する情報をデータ構造としてまとめていない。OOP の考えは全く入っていない。
- task ID を使っている。これはグローバル変数の性格を持つ
- task ID 番号を誤り wup_tsk(5) と誤った記述ができてしまう。
- task をライブラリにしようとすると、task ID を gloval 変数にせざるを得ない。task ID を使うことは、カプセル化を弱くする
- pClassTCBInstanse->wup_task(), ro C の範囲では wup_tsk(&classTCBInstanse) といった記述ができるべき。
- データ構造の概念がない。タスク制御関連のデータを TCB データ構造体などにまとめるべきである。それらをタスクごとの内部変数とすべきである。
- タスク先頭アドレス配列、タスク・スタック配置配列、
- タスクを生成・消滅させる概念がない
- polling の概念がない
- 割り込み 10--500uSec ポーリング:1mSec-10mSec タスク:5mSec-50mSec の応答時間。一つしかない CPU に対して、優先順位に応じて、割り込み・ポーリング・タスクそれぞれに処理を割り振る。
- 釦が人間によって押されたことを、割り込みを使って検出する必然性はない。そのために、タスクを一つ作る意味もない。ポーリングにわりふるべきである。
- 割り込み禁止期間が発生する
対策案
- 構造体(クラス) ClTCB を設けて、そのメンバーにタスク・スタック、タスク開始アドレスなどを纏める
- 生成した ClTCB インスタンスは list に登録する。
- ClTCB インスタンスの this 引数を、wait などのタスク制御関数の第一引数とする。(C++ では自動的に実現されてしまう)
- ClTCB に polling() メンバー・メソッドを含める
- 割り込み処理の中からタスク制御関数を呼び出すことを禁止する