平成27年 6月 13日
複数タスクに対応できるコルーチンを
USARTのような周辺機能へのアクセスをCH番号を引数にしてアクセスするようにした結果、当然のように発生する問題は現在のコルーチン実装では、複数のタスクからの同時利用は出来ない。つまりリエントラントな関数を構成できないという問題が出てきます。
コルーチンについては、こちらで説明してあります。
問題は、コルーチン変数を関数内部でstatic変数として確保している点です。これを複数の呼び出し先ごとに別個に確保する必要があります。一個のCPUで2CHの通信を同時に使用することは珍しくも無いので標準のライブラリとするには、出来ればリエントラントにしておきたいものです。
少しだけ反対意見を
リエントラントにするためには、その分プログラムが複雑になります。もし同時使用数が2CHのみと分かっている場合や、納期的にプログラムを変更する時間が取れないときに無理をしてまでプログラムを変更するのは考え物です。
暫定対応として、通信プログラム内の関数を全てコピーして別名の関数とするだけでも2CH分の通信ルーチンを確保することは出来ます。例えば通信ルーチンがUsartCom()であった場合、この関数をそっくりコピーしてUsartCom_2()という名前でコンパイルすれば、2CH目の通信ルーチンが出来上がります。これでも望みの動作は実現できるのです。
今まで、イモズル式コルーチンを使用してきたのも、それで特に支障が無かったためです。何にでも対応できるプログラムは確かに魅力的ですが、保守の負担を増やしてまでやる価値があるかどうかは慎重に判断すべきです。
今回は、将来的に使用するとしてリエントラント化を進めます。
リエントラント化するためには、コルーチン部分だけを変更しても駄目で、関数内部でstatic宣言している変数全てを、関数の呼び出し先ごとに別個に確保する必要があります。例えば、通信エラーが起きたときの繰り返し回数(現在の繰り返し数)を保持する変数なども、呼び出し先ごとに用意する必要があります。
結局は複数の変数を通信ルーチンに渡す必要があるということです。従って、次のような構造体を作って、これを通信ルーチンに渡します。下記はプログラムのイメージです。詳細は順次説明していきます。
typedef struct {
CO_STACK_t stack; //コルーチンで使用する変数領域, 後で説明する
WORK_t work; //関数内部で使用するstatic変数領域
OTHER_t other; //その他の処理で使用する変数領域
} COM_PARA_t;
int UsartCom(COM_PARA_t *para, ...)
{
//この関数内ではstatic変数を宣言せず、全て構造体WORK_tに宣言して使用する
//コルーチンは従来とは異なる実装となる
CO_start(para->stack);
CO_resetChild(para->stack); //子のコルーチン変数を初期化する
func_child(para, ..);
CO_susp(0);
CO_finish();
}
int func_child(COM_PARA_t *para, ...)
{ //子の関数内にもstatic変数は置けないのでWORK_t構造体に宣言する
CO_start(para->stack);
CO_finish();
}
まず、通信関数の変数を集めた構造体(上記ではCOM_APRA_t)を宣言します。この中には、通信ルーチン内部で宣言される全てのstatic変数やコルーチン関連の領域が含まれています。static変数はUsartCOm()関数内だけではなく、この関数から呼び出される子や孫の関数が必要とするstatic変数を含んでいます。上記では、それらを束ねてWORK_t構造体としています。
また、コルーチン呼び出しも従来のイモズル式の方法とは異なるので名前を変えてあります。
このような実装とするためには、コルーチン変数を構造体CO_STACK_t内に束ねる必要があります。従来のイモズル式ではネストの段数に制限は無かった(これが最大の魅力でした)のですが、先に変数領域を確保する必要がある今回の方式では、ネストの深さを先に決めてそれ以上の呼び出しが起きた場合には暴走しないように保護する必要があります。
実際のコードを示します。こちらはヘッダーファイルです。
#define CO_VAL_t volatile uint16_t
/* スタック方式のコルーチン
* 変数stackを保管する領域が必要
* STACK_DEPTHは個々の処理のネストよりも深く定義が必要
*/
typedef struct {
CO_VAL_t *co;
uint8_t next;
uint8_t limit;
uint8_t max;
} CO_STACK_t;
#define CO_start(stack) { \
CO_VAL_t *p__ = CO_startLow(stack); \
if(p__ == NULL) SE_fatalErr("CO_start"); \
CO_STACK_t *stack__ = stack; \
switch(*p__){ \
case 0:
#define CO_susp(ret) do{ \
*p__ = __LINE__; \
if(CO_suspLow(stack__) == false) \
SE_fatalErr("CO_susp"); \
return ret; \
case __LINE__:; \
}while(0)
#define CO_finish() \
break; \
default: \
SE_fatalErr("CO_deflt"); \
} \
*p__ = 0; \
if(CO_suspLow(stack__) == false) \
SE_fatalErr("CO_finish"); \
}
CO_STACK_t *CO_create(uint8_t depth);
void CO_resetChild(CO_STACK_t *stack);
CO_VAL_t *CO_startLow(CO_STACK_t *stack);
bool CO_finishLow(CO_STACK_t *stack);
自作の関数が見えています。SE_fatalErr()は、ほぼabort()関数と同じです。
マクロ内で呼ばれている関数の内容を下記に示します。ネストの深さは領域を確保する最初に引数として与えます。
ここにも自作の関数があります。MEM_getMem()はstatic変数として確保したメモリ領域を要求に応じ分割して割り当てるメモリ管理です。割り当てはありますが、開放はありません。非常に単純なメモリ管理です。
処理の流れとしては、CO_start()が呼ばれる度にコルーチンのネストを+1し、CO_susp()かCO_finish()が呼ばれるとネストを-1しています。後はネストが指定以上に深くなる時と浅くなりすぎる時にエラーを返す処理をしています。
CO_STACK_t *CO_create(uint8_t depth)
{
CO_STACK_t *ret = (CO_STACK_t*)MEM_getMem(sizeof(CO_STACK_t));
if(ret == NULL)
return NULL;
ret->co = (CO_VAL_t*)MEM_getMem(sizeof(CO_VAL_t) * depth);
if(ret->co == NULL)
return NULL;
ret->next = 0;
ret->max = 0;
ret->limit = depth;
return ret;
}
void CO_resetChild(CO_STACK_t *stack)
{
if(stack->next >= stack->limit){
SE_fatalErr("1");
}else{
stack->co[stack->next] = 0;
}
}
CO_VAL_t *CO_startLow(CO_STACK_t *stack)
{
CO_VAL_t *ret;
if(stack->next >= stack->limit){
return NULL;
}else{
if(stack->next > stack->max)
stack->max = stack->next;
ret = &stack->co[stack->next++];
}
return ret;
}
bool CO_suspLow(CO_STACK_t *stack)
{
if(stack->next == 0){
return false;
}
stack->next--;
return true;
}
これでコルーチンもリエントラント化できました。