4

我已经使用 C 的 switch 语句为嵌入式系统实现了一个简单的状态机。我知道如果我使用带有查找表的函数指针会更好,但我将其保存为下一步。

我的状态机有以下状态:

  1. 启动(初始状态)
  2. 启动错误。
  3. 空闲(系统仅在此状态下检查输入。它不会更新显示或其他任何内容。它只是“空闲”)。
  4. 检查(这是实际应用)
  5. 程序
  6. 复制到内存

当系统启动时,它进入配置端口的启动状态,初始化显示并与连接在 SPI 总线上的 IC 握手,以确保一切正常。如果是,则系统进入空闲状态。如果不是,它进入启动错误状态,在 LCD 上显示一个错误,标记一个变量,然后进入空闲状态。

在空闲状态下,程序轮询微控制器上的 3 个引脚以检查 3 个按钮(检查、程序、复制到内存)中的一个是否被按下。根据按下的按钮,它会进入适当的状态,执行一些代码,更新 LCD,然后返回空闲状态。注意:如果系统中存在硬件故障,系统不关心是否按下按钮。启动错误状态标记一个名为 hardware_fault 的变量,如果设置该变量,则确保空闲状态不会打扰轮询任何输入按钮。

这是我第一次实现状态机,我只是不确定这是否是一个好的设计。我还没有真正看到 FSM 在空闲状态下轮询输入的任何示例。相反,似乎大多数示例本质上是相当连续的(例如计数器)。所以,我的问题是,我的设计合理吗?它确实有效,但正如这里的每个人都知道的那样,有糟糕的设计,然后有好的设计。

4

1 回答 1

7

您应该将代码从 switch 语句中重构出来,并使用您提到的转换表。围绕开关的代码无法扩展、混乱并且很快变得难以阅读和更新。

为了回答你的问题,我想说大多数状态机实际上和你的一样:它们对事件做出反应(在你的情况下是民意调查)。如果它们是纯顺序的,根本没有事件,它们是用于特殊用途的,比如实现一个正则表达式......

需要注意的是,Idle状态相当于状态机库的运行时核心:它拦截事件并将它们发布到状态机。使用状态机库,它将对客户端代码“隐藏”,而像您这样的基本实现必须明确地进行事件轮询。

现在是一个设计批评:一般来说应该避免全局变量,尤其是在状态机中。StateIdle不应该知道hardware_fault全局变量。对于状态机,人们应该努力通过添加状态(当它有意义时!)将全局变量“嵌入”到状态机本身中。对(分层)状态机的一个很好的介绍和基本原理的解释在这里

使用 UML 表示法(参见此处的教程),您的状态机是: 状态机 v1

一个简单的重构来完全删除hardware_fault依赖项是: 状态机 v2

也就是说,我们只是永远停留在StartupError状态中,而不转换到Idle.

ε (epsilon) 表示一个空转换,即一旦与源节点关联的活动结束就进行转换,没有任何事件。这就是您所说的“顺序”状态机。

我还包括了第一个图表的来源(第二个非常相似),它是用非常简单和强大的PlantUML生成的。

@startuml

skinparam shadowing false
title Embedded v1

state Startup
state StartupError
state Idle
state Program
state Check
state Copy

Startup : entry/ init HW
StartupError: entry/\n  display error\n  hw_fault = true
Idle : do/ if not hw_fault: poll
Program : do/ program
Check : do/ check
Copy : do/ copy

[*] -> Startup

Startup -> StartupError : ε [error]
Startup --> Idle : ε [not error]

StartupError --> Idle : ε

Idle --> Program : program_btn
Idle --> Check : check_btn
Idle --> Copy : copy_btn

Program --> Idle : ε

Check --> Idle : ε

Copy --> Idle : ε

@enduml
于 2012-07-22T19:46:16.370 回答