スパイス  組み込み制御装置の受注製作

Z80にマルチタスク機能を
平成26年4月29日

 はじめに
 モニタプログラムを移植することで、xprintf()関数によるデバッグに目処が立ちました。また、Hitech-Cコンパイラを移植することでGAIO-Cコンパイラに多少の問題があっても、それを検証する代替的な手段を確保することが出来ます。
 次はアプリケーションプログラムの開発負担を軽くすることを考えます。組み込みプログラムで欲しい機能はいくつかありますが、やはりマルチタスク機能が最有力でしょう。

  マルチタスクの実現方法
 一般にマルチタスクの実現方法はタスクコンテキストを切り替えるタスク切り替え方式が主流ですが、他にも方法はあります。特に小規模なシステムやハードウェアの性能が低い場合には、コルーチン方式の方が向いています。

1.コルーチン
 数千行以下の比較的小規模なプログラムであればコルーチンだけで全体のマルチタスクを構成することも可能です。この方式のメリットはハードウェアスタックが固定されているPIC16やPIC18などでも使用できることです。コルーチンとはC言語の関数とは異なり、直前に処理を中断した位置の次から処理を再開できる機能モジュールです。コルーチンの動作を示すための簡単なサンプルを示します。
void coroutine_1(int *co_tag)
{
    printf("\tfunction pass\n");
    CO_begin(*co_tag);
        printf("coroutine pass 1\n");
        CO_yield();
        printf("coroutine pass 2\n");
        CO_yield();
        printf("coroutine pass 3\n");
    CO_end();
}

int co_variable;
void main(void)
{
    for(;;){
        coroutine_1(&co_variable);
    }
}
 メイン関数ではcoroutine_1()関数を繰り返し呼び出しています。coroutine_1()の処理内容は、毎回”\tfunction pass”を出力後、コルーチン処理を行います。CO_begin()関数がコルーチン動作の開始を、CO_end()関数が終了をそれぞれ示します。コルーチンの動作は直前に動作を中断した位置の次から処理を再開します。中断はCO_yield()関数が行います。つまり、2回目以降は以前の処理を継続したように見えます。
 このプログラムの実行結果は下記になります。
    function pass
coroutine pass 1
    function pass
coroutine pass 2
    function pass
coroutine pass 3
    function pass
coroutine pass 1
    function pass
coroutine pass 2
(以下省略)
 coroutine_1()自体は関数ですので、呼び出される度に関数の先頭から処理を開始します。従って、関数呼び出しの度に"\tfunction pass"が出力されます。しかし、CO_begin()によって動作がコルーチンに変更され、CO_end()に出会うまで継続します。最初の呼び出しでは、CO_begin()の直後から実行されCO_yield()またはCO_end()に行き当たるまで処理が行われます。ここで”coroutine pass 1”が出力されます。CO_yield()はC言語のreturn文に相当し、コルーチンを抜けると同時にコルーチンを内包している関数からも抜け出します。
 2回目のcoroutine_1()関数の呼び出しでは、CO_begin()に行き着くまでは従来と同じです。しかし、CO_begin()によって動作がコルーチンとなるので、先に処理を中断したCO_yield()の次に処理が移動します。この場合は”coroutine pass 2”の出力に移動します。
 最後のCO_end()に到着するとコルーチン動作が初期化されます。つまり次にcoroutine_1()関数が呼び出されたときには、初めてコルーチンが呼ばれたものとしてCO_begin()の次から処理を行います。

 この機能を使用すれば複数の処理を見かけ上平行して動作させることが出来ます。メイン関数のforループ内で複数のコルーチン関数を順番に呼び出すようにします。ここで注目すべきはco_variableNという変数です。実際の処理中断位置はこの変数に記憶されています。
int co_variable1,....,co_variableN;
void main(void)
{
    for(;;){
        coroutine_1(&co_variable1);    //タスク1
        coroutine_2(&co_variable2);    //タスク2
        (省略)
        coroutine_N(&co_variableN);    //タスクN
    }
}

2.処理の中断
 組み込み装置にコルーチンを適用することを考えると、常に継続実行しか出来ないのは問題になります。機械制御では継続実行以外にも処理を中断したいことがあります。この場合には先の変数co_variableNを初期化します。

3.C言語の関数実装に合わせたコルーチンの分割
 C言語では処理の多くは複数の関数に分割して実装されます。このため単独の関数をコルーチンに仕立てても上手くいきません。複数の関数をコルーチン化して、それらをイモズル式に制御します。組み込み制御の多くは処理の中断後は最初から実行しますので、親のコルーチンが初期化されたら無条件に子のコルーチンも初期化するようにプログラムします。
bool func_child(int *);

void func_parent(int *co)
{
    static int co_child;  //co_childはこの関数を離れても以前の値を保持する必要があるのでstatic宣言が必須
    CO_begin(co);
        co_child=0;    //ここが実行されるのは、親が初期化された時、親が初期化されたら子も初期化する
        while(func_child(&co_child) == false){  //func_child()がtrueを返すまで、次の処理には進まない。
            CO_yield();
        }
        (次の処理)
    CO_end();
}
bool func_child(int *co_child)
{
    CO_begin(co_child);
    (処理)
    CO_end();
}

4.コルーチンを使用する上での注意点
 コルーチンはルールが単純でわかり易いのがメリットです。反面、注意が必要になるのは関数内でのローカル変数の扱いです。C言語上での実装では関数を見かけ上コルーチンに見せかけているので、コルーチンとしては不完全です。特に関数を一旦抜け出るCO_yield()を挟む前後でのローカル変数の値保持には要注意です。CO_yield()でコルーチン(および関数)を抜けた段階で、ローカル変数の値は失われています。このような変数には全てstatic宣言が必要です。変数管理が煩雑になるリスクを考慮すると、コルーチンを内包する関数では全てのローカル変数をstatic宣言するというルールを徹底した方が安全です。
bool func_child(int *co_child)
{
  int i,j;          //static宣言されていない
    CO_begin(co_child);
        i = 0;        //ここでは変数iの値は0;
        CO_yield(false);  //関数を抜け出るので変数iの値は失われる
        j = i;        //変数iの値は不定
    CO_end();
    return true;
}
(2015/06/13追記)
 リエントラント化したコルーチンの記述を追加しました。こちらにあります
(追記終了)
目次へ 前へ 次へ