应用程序设计模式:状态机

概览

状态机是LabVIEW开发人员经常用来快速构建应用程序的基本架构之一。状态机架构可用于实现复杂的决策算法,决策算法可由状态图或流程图表示。可以使用固有的LabVIEW函数来实现状态机。该架构不需要其他工具包或模块。

 

本文介绍了什么是状态机、状态机的应用场景范例、状态机的一些概念性范例,以及状态机的代码范例。

内容

什么状态机?

状态机是一种编程架构,支持根据以前的状态或用户输入的值动态流向状态。

此架构适用于符合以下所有情况的应用程序:

  • 状态
  • 决策逻辑,用于确定何时转移至特定的状态


状态可定义为在完成整体编程任务过程中的编程状态,比如,初始化、等待、运行计算、检查状态等。

逻辑语句有助于确定何时转移至新状态以及转移至何种状态。事件可用于触发从一种状态到另一种状态的转移,可以是编程事件,也可以是用户定义的事件,如按下按钮。

状态机中的每个状态都有独特的作用,也可调用其他状态。状态通信受某些条件或序列影响。要将状态程序框图转换为LabVIEW编程架构,您需要以下基础结构:

  1. While循环 — 不断执行各种状态
  2. 条件结构 — 每个条件都包含要针对每个状态执行的代码
  3. 移位寄存器 — 包含状态转换信息
  4. 转移代码 — 确定序列中的下一个状态(请参见下面的转移代码范例部分)

为何使用状态机?

状态机适用于存在不同状态的应用程序。每个状态都可能触发另一个状态或多个状态,也可能结束流程。状态机按照用户的输入或状态内计算来确定下一个状态。许多应用程序需要一个“初始化”状态,后跟默认状态。默认状态可执行多种不同的操作。执行的操作取决于上一个状态及当前的输入和状态。“关闭”状态可以用于执行清理操作。

除了具有实现决策制定算法的强大能力之外,状态机还以功能的形式体现了应用程序规划。随着应用程序越来越复杂,对完善设计的需求也在增加。状态程序框图和流程图对设计有所助益,有时甚至是必不可少的。状态机不仅在应用规划方面颇有优势,而且也很容易创建。

用例

例如,对下列应用程序采用状态机模式非常有效:

  • 单个页面或包含多个选项卡的对话框。对话框的每个选项卡对应一种状态。用户单击特定的选项卡时,启动状态转移。对于每个选项卡,用户可执行的操作都包含在相应的状态中。
  • 自动取款机(ATM)。该应用程序可能涉及以下几种状态:等待用户输入、检查请求的金额是否超过账户余额、吐钞、打印收据等。
  • 执行一次测量、将结果记录至磁盘并等待其他用户操作的应用程序。该应用程序可能涉及以下几种状态:等待用户输入、执行测量、记录数据、显示数据等。
  • 在用户界面进行编程时,经常用到状态机。创建用户界面时,不同的用户操作会将用户界面送入不同的处理段。每个处理段都将作为状态机中的状态。这些段可触发其他段进行进一步的处理,或者等待其他用户事件。在本范例中,状态机持续监控用户将采取的下一个操作。
  • 流程测试是状态机的另一种常见应用。在此范例中,每段流程都由一个状态表示。根据每个状态测试的结果,可能会调用不同的状态。上述操作可连续发生,对测试进程执行深入分析。


还有另一种可用于实现用户界面的设计模式,即队列消息处理器。队列消息处理器是一种更复杂的状态机版本,灵活性更高,但同时也更为复杂。 

创建状态机

创建有效的状态机需要设计人员(1)列出可能的状态。根据此列表,设计人员可以(2)规划状态间的关联性。然后,将状态程序框图(3)转换为LabVIEW图形化编程架构。

例:发射大炮

在此范例中,我们想生成一个应用程序,可连续发射大炮而不会因过热引发危险。

(1)   列出可能的状态
首先,我们为任务列出所有可能出现的状态。为了完成连续发射大炮的任务,需要:

  • 初始化程序
  • 启动大炮
  • 发射大炮
  • 检查设备温度(状态)
  • 被动冷却设备(如果温度仍在正常范围内但不断升高)
  • 主动冷却设备(如果温度超出正常范围)
  • 关闭设备
     

(2)   在状态程序框图中绘制状态之间的关系
接下来,我们将考虑这些状态之间的关联性并创建状态程序框图。在创建状态程序框图时,请考虑会导致程序从一个状态转移到下一个状态的原因是什么?是自动转换吗?是否需要用户外部触发?是否基于计算结果实现转换?

例如,我们来考虑一下初始化状态和其他状态之间的关系。

  1. 初始化是程序的第一步。此状态没有输入。
  2. 一旦初始化后,如果没有错误,我们将继续启动大炮。
  3. 如果出现错误,则会关闭程序
  4. 如果用户在初始化过程中按下了停止按钮,我们希望机器进入关闭状态。

请注意,在思考状态之间的关系时,我们已经开始定义这些状态之间的转移逻辑。我们将在代码中使用的编程逻辑取决于(1)可能的转移状态数,以及(2)逻辑中要考虑的输入数。代码转移的选项将在下面的转移代码范例部分中进行讨论。

继续研究状态之间的相互关系,直至得到完整的状态程序框图。状态程序框图将包括所有状态以及它们之间的关系(图1)。请注意,在此程序框图中,状态(椭圆形节点)描述了控制过程处于该状态时所执行的操作,而转移(箭头)仅描述了过程何时以及如何从一种状态转移到另一种状态。

图1:发射大炮的状态程序框图

每种状态之间的关系有助于对从一种状态转移到另一种状态所需的逻辑进行编程。

(3)在LabVIEW中构建状态机
在状态程序框图中定义了状态及其关系之后,可以将其转换为LabVIEW中的编码架构(图2)。状态程序框图(图1)中状态之间的流动由循环实现。各个状态将由条件结构中的条件替换。上图中的每一种状态对应于条件结构的一个子程序框图。每种状态:

  1. 执行某项操作
  2. 通过向While循环的移位寄存器传递指令(即转移代码),指定状态机的下一个状态


While循环的移位寄存器会跟踪当前状态,将其馈送到条件结构输入中。

图2:状态机

状态程序

关于修改此模板以用于测量应用程序的范例,请参见“创建项目”(Create Project)对话框中的“单次测量范例项目”(Single Shot Measurement sample project)。

  • 初始化完成后,状态机转移至“等待事件”(Wait for Event)状态。该状态通过事件结构等待前面板发生改动。用户单击按钮时,由LabVIEW识别该事件,然后切换至事件结构的相应子程序框图。接着由该子程序框图发起状态转移,转移至相应的状态。
  • 每种状态可以访问一个数据簇。此簇中包含的数据类型通过Data.ctl定义。
  • State.ctl是一个自定义类型,罗列了有效的状态。用自定义类型实现状态转移的方法限制了可使用的转移操作数量,减小了状态机陷入不可识别状态的可能性。
  • 只有“停止”(Stop)状态可以使应用程序停止运行。这种设计可以避免应用程序意外关闭或不完全关闭,原因是:
     

1.     仅当用户希望停止应用程序时,才会运行关闭代码。
2.     关闭代码始终运行,直到彻底完成。

  • 同一时间只运行一种状态,由于采用了单个While循环,所有任务按相同速度执行。如需实现按多种速度执行或并行执行的任务,可考虑采用“队列消息处理器”或“操作者框架”模板,上述模板均可在“创建项目”(Create Project)对话框中找到。
  • “等待事件”(Wait for Event)状态是唯一能识别用户输入的状态。在接收用户输入时,状态机必须处于该状态下。

创建自己状态应用程序

要获得入门指导,请查阅修改简单状态机LabVIEW模板教程。 

确定需求

对本模板进行自定义之前,应确定以下问题:

  • 应用程序包含哪些状态? 此问题的答案决定了要添加的状态。
  • 每个状态的下一个状态是什么? 此问题的答案决定了“下一个状态”(Next State)枚举的值。每个状态都会将该枚举值发送至While循环的移位寄存器。

    一个状态可在某种条件下转移至多个状态。例如,模板的“等待事件”(Wait for Event)状态可根据用户输入转移至另一状态。
  • 每个状态需要访问哪种数据? 此问题的答案决定了添加到Data.ctl中的数据类型。
  • 可能会发生什么错误,以及应用程序应如何应对这些错误? 这些问题的答案决定了所需的错误处理量。
     

转移代码范例

确定下一个要转移到哪个状态的方法有很多,下面将对此进行讨论。
请注意,虽然范例图片显示的是“Init”状态,但这些转移的可能性适用于任何状态。

一对一:如果总是从状态A转移到状态B,则不需要对任何逻辑进行编程,只需将下一个条件(条件B)的名称输出到移位寄存器即可。

图3a:只有一种可能的转移状态

一对二:如果需要从状态A转移到状态B或状态C,则可以使用选择函数来评估某个显示控件的状态。您应该评估一些决定您想转移到哪个状态的因素。例如,在图3b中我们看到,用户的“停止”(Stop)按钮输入决定了我们是否从“开机”(Power Up)状态转移到“关闭”(Shut Down)状态。

图3b:两种可能的转移状态

一对多(使用数组):如果有多个可以作为转移目标的状态,则可以使用与枚举常量相关的布尔区域来对该转移进行编程。例如,图3c中执行了一些代码,代码的结果决定了转移,而转移的输出是一个布尔数组。布尔数组与枚举常量相关,该常量包含一个可能过渡到的目标状态列表。使用索引数组函数,系统将输出布尔数组中第一个“True”布尔值的索引。然后,利用数组子集函数,从与之相关的枚举常量中提取相关内容。

图3c

提示:回顾一下,数组是0索引,而枚举是1索引。为了使布尔数组与枚举常量相关联,可使用加1函数来纠正这一偏移。

一对多(使用While循环):如果有多个可能的转移状态,则可以在条件中使用While循环。While循环内的代码将继续运行,直到布尔状态被设置为True,触发停止按钮。这将在发生触发事件前,有效地支持代码运行。

图3d展示了使用内循环和条件结构转移到下一个状态的“Init”状态。内部条件结构包含了一种程序框图,涵盖每个离开当前状态的转移。内部条件结构中的每个案例都有两个输出:一个是布尔值,指定是否应该进行转换;另一个是枚举常量,指定转换到的目标状态。将循环索引用作条件结构的输入后,该代码可有效地逐一运行每个转移条件,直至找到一个布尔输出为“True”的程序框图。在找到“True”布尔输出后,该条件就会输出要转移到的新状态。虽然这个方法看起来可能比前面的方法略微复杂,但它确实能够将循环索引的输出“强制转换”为一个枚举类型,以此为转移添加名称。借助这一优势,便可在转移代码中添加“自动文档”。

图3d

其他事项

 

代码冗余
问题:创建状态机最难的部分是区分状态程序框图中的可能状态。例如,在可乐售卖机的状态程序框图(图4)中,可能有0、5、10、15、20、25、30、35、40、45、50美分的状态,而不是只有一个“等待响应”的状态,从一个状态到另一个状态取决于投掷的硬币类型。那么,同样的条件程序框图会创建出11种不同的状态。冗余的代码会在较大的应用程序中造成很大的问题。

解决方案
:如果不同的状态具有相同的条件程序框图,请尝试将其合并为一个状态。例如,创建“等待响应”状态是为了避免代码冗余。

枚举使用
问题:枚举作为条件选择器在状态机中得到了广泛使用。如果用户试图向/从这个枚举中添加或删除一个状态,连线至该枚举副本的其他连线将会断开。这是用枚举实现状态机时最常见的阻碍之一。

解决方案:可通过两种方法解决该问题:
1.如果所有枚举都是从更改后的枚举中复制而来,那么断点将消失。
2.用枚举创建一个新的控件,并从子菜单中选择“自定义类型”(typedef)。选择“自定义类型”(typedef)后,如果用户添加或删除一个状态,所有的枚举副本将会自动更新。

下一步

如果您有兴趣详细了解状态机和其他LabVIEW高级架构,可以考虑参加LabVIEW核心教程(二)客户培训课程。

Was this information helpful?

Yes

No