Cステートマシン設計

以前に設計したステートマシンは、すべてstruct配列とループになりました。構造は基本的に、状態とイベント(ルックアップ用)、および次のような新しい状態を返す関数で構成されます。

typedef struct {
    int st;
    int ev;
    int (*fn)(void);
} tTransition;

次に、単純な定義で状態とイベントを定義します(ANYのものは特別なマーカーです。以下を参照):

#define ST_ANY              -1
#define ST_INIT              0
#define ST_ERROR             1
#define ST_TERM              2
: :
#define EV_ANY              -1
#define EV_KEYPRESS       5000
#define EV_MOUSEMOVE      5001

次に、遷移によって呼び出されるすべての関数を定義します。

static int GotKey (void) { ... };
static int FsmError (void) { ... };

これらの関数はすべて、変数を受け取らず、ステートマシンの新しい状態を返すように記述されています。この例では、必要に応じて状態関数に情報を渡すためにグローバル変数が使用されます。

FSMは通常単一のコンパイルユニット内でロックされ、すべての変数はそのユニットに対して静的であるため、グローバルの使用は見た目ほど悪くありません(これが上記の「グローバル」の周りに引用符を使用した理由です。 FSM、真にグローバルよりも)。すべてのグローバルと同様に、注意が必要です。

次に、transitions配列は、考えられるすべての遷移と、それらの遷移に対して呼び出される関数(最後のすべてを含む)を定義します。

tTransition trans[] = {
    { ST_INIT, EV_KEYPRESS, &GotKey},
    : :
    { ST_ANY, EV_ANY, &FsmError}
};
#define TRANS_COUNT (sizeof(trans)/sizeof(*trans))

つまり、ST_INIT状態にあり、EV_KEYPRESSイベントを受け取った場合、GotKeyを呼び出します。

FSMの動作は、比較的単純なループになります。

state = ST_INIT;
while (state != ST_TERM) {
    event = GetNextEvent();
    for (i = 0; i < TRANS_COUNT; i++) {
        if ((state == trans[i].st) || (ST_ANY == trans[i].st)) {
            if ((event == trans[i].ev) || (EV_ANY == trans[i].ev)) {
                state = (trans[i].fn)();
                break;
            }
        }
    }
}

 

上記で示唆したように、ST_ANYをワイルドカードとして使用し、現在の状態に関係なくイベントが関数を呼び出すことができることに注意してください。 EV_ANYも同様に機能し、特定の状態のイベントが関数を呼び出すことを許可します。

また、transitions配列の最後に到達した場合、FSMが正しく構築されていないことを示すエラーが表示されることを保証できます(ST_ANY/EV_ANYの組み合わせを使用します)。

組み込みシステムの通信スタックやプロトコルの初期実装など、非常に多くの通信プロジェクトでこれに似たコードを使用しました。大きな利点は、そのシンプルさと、トランジションの配列を比較的簡単に変更できることでした。