小编典典

C 状态机设计

all

我正在制作一个混合 C 和 C++ 的小项目。我正在我的一个工作线程的核心构建一个小型状态机。

我想知道您的 SO 专家是否会分享您的状态机设计技术。

注意: 我主要是在经过尝试和测试的实施技术之后。

更新: 基于 SO 上收集的所有重要输入,我已经确定了这个架构:

事件泵指向事件集成器,事件集成器指向调度器。 调度程序指向 1 到 n 个动作,这些动作又指向事件集成器。
带有通配符的转换表指向调度程序。


阅读 68

收藏
2022-07-04

共1个答案

小编典典

我之前设计的状态机(C,不是 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,而不是真正的全球性)。与所有全局变量一样,它需要小心。

然后转换数组定义所有可能的转换以及为这些转换调用的函数(包括最后一个全能转换):

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也类似地工作,允许处于特定状态的任何事件调用函数。

它还可以保证,如果您到达转换数组的末尾,您会收到一个错误,说明您的 FSM 没有正确构建(通过使用ST_ANY/EV_ANY组合.

我在很多通信项目中都使用过类似的代码,例如早期实现的通信堆栈和嵌入式系统协议。最大的优点是它的简单性和更改转换数组的相对容易性。

我毫不怀疑会有更高级别的抽象,现在可能更合适,但我怀疑它们都会归结为这种相同的结构。


而且,作为ldog注释中的状态,您可以通过将结构指针传递给所有函数(并在事件循环中使用它)来完全避免全局变量。这将允许多个状态机并排运行而不受干扰。

只需创建一个结构类型来保存特定于机器的数据(至少是状态)并使用它而不是全局变量。

我很少这样做的原因仅仅是因为我编写的大多数状态机都是单例类型(例如一次性、进程启动、配置文件读取),不需要运行多个实例.
但是,如果您需要运行多个,它具有价值。

2022-07-04