首先让我们来了解一下 main()函数程序架构,如下图 1 所示:
main()以对 MainInit()的调用开始,MainInit()是一个函数,它执行所有程序和系统的初始化(稍后将在手册示
例中详细介绍)。在完成所有初始化之后,调用 MachineSequences()函数。该函数有意地启动机器序列和
运动的执行。在机器运行期间,MachineSequences()函数不会返回到 main(),直到程序请求终止(由于错误,
用户请求关闭,等等)。当机器操作完成时,machine Sequences()函数返回 main(),main()调用 MainClose()
函数来关闭程序终止之前需要关闭的所有内容。
这是程序的 main()函数。尽可能简单和干净。现在我们进入 MachineSequences()函数,使用状态机结构来
查找机器序列的实现。
下图 2 是关于 machineSequences()函数介绍:
请注意,红色的代码块是一段需要尽可能快地执行的代码,不应该包含任何执行时间或延迟相对较长的进
程。当我们深入研究程序结构时,这将变得特别相关。显然,它不应该包含任何无休止的循环或任何等待
系统进程结束的过程。一个红色的块应该包含一段代码,它无条件地执行有限大小的代码,没有延迟或等
待。
MachineSequences()从调用 MachineSequencesInit()开始。此函数初始化所有变量,以激活
MachineSequencesTimer()并管理状态机(参见图 2)。
后立即,MachineSequences()调用 EnableMainTimer (TIMER_CYCLE(记住,格式(CAPITAL_LETTERS)是
指一个常数由程序员定义的头文件))开始执行 MachineSequencesTimer()函数来定义,它将自动执行的操作
系统(OS)每个 TIMER_CYCLE 女士作为这些描述的典型值,假设一个 TIMER_CYCLE = 20 毫秒。
从现在开始,OS 每隔 20ms 激活 MachineSequencesTimer()。这个定时器函数实际上处理和管理状态机,
如下所述。
函数现在进入一个无限的 while 循环,等待一个全局变量(giTerminate)来指示 MachineSequencesTimer()
请求终止程序。当启动 MachineSequencesInit()函数时,giTerminate 变量将初始化为 FALSE,如果需要(程
序可能永远不会终止),由 MachineSequencesTimer()函数可选地将其设置为 TRUE。
一般来说,这个无尽的 while 循环只需要等待终止请求。为了不只是为了在循环中运行而加载 CPU,在循
环中插入了 Sleep(SLEEP_TIME)。SLEEP_TIME 的一个典型值是 100ms,这意味着(在我们的示例中)这
个后台循环大约每 5 个周期激活一次计时器函数(该函数每 20ms 激活一次)。
注意定时器方法是一种精确的方法,用于在每个给定的周期内创建对定时器函数的调用。Sleep()方法并不
精确,但是对于这个空闲循环,计时精度不是问题。
最后,由于我们这里有一个后台循环代码,它以相对较低的速率周期性地激活,所以我们可以使用它来执
行一些时间不那么关键的流程,这可能是应用程序所需要的。进程,您可能不希望将其包含在由定时器函
数确定执行的主状态机中。这就是为什么我们在这个 while 循环中有可选的 BackgroundProcess()函数。
在请求终止之后,while 循环立即结束,然后调用 MachineSequences()来关闭需要关闭的所有内容,然后
返回 main()函数来终止程序。为什么用引号“立即”?因为响应时间可能与此循环的 SLEEP_TIME 一样长。
然而,在处理终止请求时,响应时间不应该成为问题。
现在,我们有一个在后台“缓慢”循环的程序——一个几乎是空闲的循环,同时触发一个计时器函数
MachineSequencesTimer(),并在每个 TIMER_CYCLE ms(在我们的示例中是 20ms)中执行状态机。
让我们深入研究 MachineSequencesTimer()函数。
下图 3 是关于 MachineSwquencesTime()函数:
图 3 给出了一个典型的 MachineSequencesTimer()函数的一般结构。
为什么“一般”?因为它没有显示状态机的详细信息。这将在稍后介绍。首先,了解 MachineSequencesTimer()
函数的一般结构。
最初,在计时器事件(如上面初始化的,每个 TIMER_CYCLE ms)上触发 MachineSequencesTimer()函数。
它的第一个操作是调用 ReadAllInputData()函数。
函数 ReadAllInputData()是一个依赖于应用程序的函数。它的任务是读取状态机可能需要的所有输入,并将
它们复制到“外部世界”无法访问的变量中。
这将确保在此计时器事件期间执行的所有状态机代码将使用相同的输入变量值。
为什么需要这样做?
由于定时器事件不一定与“外部世界”操作同步,例如,主机可以访问 MODBUS 内存并修改状态机代码使
用的寄存器之一。同样,白金大师核心可以获得一个新的读数,例如驱动器的速度通过设备网络。因此,
外部环境可以在 MachineSequencesTimer()执行期间更改这种“输入数据”,从而导致代码流的操作不一
致。首先需要将所有必要的值复制到“镜像变量”中,然后才开始使用这些镜像变量,这些镜像变量将保
持不变,直到下一次计时器事件。
这正是 ReadAllInputData()函数的任务。根据应用程序的不同,它应该访问所有必要的变量(MODBUS 内存
的变量,从 Platinum Maestro 固件内核等),并将它们复制到“镜像变量”中。
注意:在 MachineSequencesTimer()的开头,使用 ReadAllInputData()函数读取和创建所有必要的“外部
世界”变量的副本是非常重要的,并且在状态机代码期间只使用这些副本或镜像。这将避免同步和不一致
代码行为中的困难。
同样,状态机不应该直接写入“外部世界”。状态机代码应该设置内部变量(状态机的变量,不在状态机外
部使用)来反映“代码决策”或编写到“外部世界”的需求。
只有当遍历所有状态机(稍后将详细解释)时,MachineSequencesTimer()才调用 WriteAllOutputData()(图 3),
该函数使用这些内部变量来编写应该写入外部世界变量的内容(MODBUS、Platinum Maestro 固件 core 等)。
程序员应该注意正确地更新“外部世界”变量(WriteAllOutputData()函数内部),在某些情况下,写入的顺序
可能很重要。例如,(程序员应该)仔细定义通过 MODBUS 的主机握手,以确保同步和完全一致的通信。
注意:写入“外部世界”变量不应该在机器状态代码中执行,而应该只在 WriteAllOutputData()函数
中执行,以确保正确的同步和一致操作。
现在让我们看看状态机本身。在图 3 中,您可以看到,在一般情况下 MachineSequencesTimer()函数可以
处理多个独立状态的机器。例如,对于需要独立管理的机器的不同子系统。每个状态机都有自己的一组状
态变量,并且每个状态机都是独立管理的(尽管特定的实现可以用另一个状态机的状态来约束给定状态机的
行为,这是特定于应用程序的)。
例如,每个轴可以有自己的状态机。
实现的另一个例子是为 XYZ 拥有一个状态机,为机器的加载器机制拥有第二个状态机。两者都是独立的,
尽管可以在第二个状态机等待进程完成后再启动自己的进程,例如:
当然,这只是一个简化的处理,但是它解释了为什么处理两个独立的状态机更容易,如图 3 所示(显示最多
N 个状态机的一般情况)。上面的示例还说明了为什么一个给定的实现可能需要用另一个状态机对给定状态
机的行为进行条件设置(它们是独立管理的,buy 可能有条件地执行)。
图 3 显示了每个状态机使用以下变量(第一个状态机的“N”从 1 开始)。
giStateN
定义状态机的当前状态。它通常由 MachineSequencesInit()函数初始化(见上面),然后可以通过
MODBUS(执行任务的主机请求)或状态机本身修改它,同时它从一个状态执行到另一个状态(进程的执行)。
giPrevStateN
将状态值保持为在前面执行 MachineSequencesTimer()函数时的状态值。使用这个变量,状态机代码(将在
后面的图中显示)可以检测 giStateN 值是否是一个“新”状态,并相应地执行(有关详细信息,请参阅后面
的内容)。
它通常与 giStateN 变量一起初始化。
giSubStateN
如果需要子状态机(请参阅后面的详细信息),则此变量定义子状态机的当前状态。它通常由
MachineSequencesInit(0 函数)初始化——就像上面的变量初始化一样。
但是,它也会被 MachineSequencesTimer()在每次请求一个新状态时重置为零(或者更好的说法是:第一个子
状态的值)(稍后会详细介绍)。
这些变量用于管理状态机和子状态机,我们将在后面的图中看到。
为什么需要子状态机?
假设一个 XY 轴的状态机。此外,假设它需要管理以下任务:HomeXY、ScanObject 和 GoToIdle。状态机基
本上有三种状态:HOME_XY、SCAN_OBJECT 和 GO_TO_IDLE。
但是,执行 HomeXY 本身是一个由一系列运动和条件组成的过程,所以它也必须作为状态机来实现。这将
作为子状态机实现。
在对 XY 进行寻的过程中,主状态机(在下一个图中出现的机器,位于 MachineSequencesTimer()函数中)
将处于 HOME_XY 状态,而子状态机(我们将在后面看到图)将进入寻的各个步骤。
理论上说(实际上,甚至几乎),这种结构的并行运行多个独立国家机器(如第二图所示)和嵌套的国家机器在另
一个(如前所述,我们将会看到在后面的数字)可以扩展并适合应用程序的要求。
在下面的图中,我们展示了 1…N 个并行状态机和一个只有两个状态机的深度(主状态机和一个子状态机)。
这只是为了简单的数字。但是,可以增加深度,并且所需的变量名称和处理的更改是次要的,应该很容易
由经验丰富的程序员处理。
注意,虽然理论上可以实现无限的并行性和深度,程序员负责确保整个 MachineSequencesTimer 的最坏情况
下的执行时间()函数将比 TIMER_CYCLE 时间短,为了不饱和铂金大师 CPU 处理负载(这是好的,如果编写代
码使用在这些章节中,描述我们的指导方针因为白金 Maestro 处理器可以处理比任何实际应用程序需要的更
多的状态机)。
记住我们在本章前面描述的 giTerminate 变量,应该很清楚,任何给定应用程序需要的任何状态机都可以设
置这个全局变量,以便请求(来自 MachineSequences()函数)将应用程序终止回操作系统。
下图 4 显示了其中一个状态机中的代码细节: