如何在Arduino上实现有限状态机

电子说

1.2w人已加入

描述

步骤1:什么是有限状态机?

Arduino

Arduino

一个有限状态机(简称FSM)是一台机器(以抽象的方式)具有定义的有限数量的可能状态,一次只能激活一个状态。状态通过转换连接。这些过渡有一个确定的方向,只能朝这个方向通过-可以将其视为一条单向街道。此外,过渡具有某些输入和输出。您可以将其视为要使用单向街道之前必须满足的条件,并且在使用单向街道时会向外界发出信号–例如,您必须先付费才能使用使用街道,然后计算您的汽车。

在许多房屋中都发现了一个非常基本的FSM示例:按一下按钮即可激活楼梯间的灯。一定时间后,指示灯会自动关闭。您可以将此模型建模为具有两种状态的FSM:“亮”和“灭”。从一种状态过渡到另一种状态,反之亦然,条件是在一个方向上按下按钮,而在另一方向上经过了一定的时间。我们可以在图表(即状态图)中对此行为进行建模。看一下图片1。

黑色实心圆圈标记着状态机的入口点(一切都必须从某处开始)。因此,如果我们的机器开始运行,则指示灯熄灭。一直这样,直到我们使用电灯开关–灯才亮起,并在30秒后熄灭。当指示灯已亮起时按按钮不起作用,并且30秒钟后指示灯熄灭。该FSM没有任何输出。从传统的数学角度来看,“点亮”状态与“打开”状态等效,但是当我们真正开始对系统进行编程时,我们当然需要添加某种实际上可以完成某些工作的输出,例如打开

此状态机有效,但这是一个好的系统吗? 30秒足够长吗?对于大多数人来说,可能是的,但是位于10楼的人们可能不喜欢我们的系统。他们可能需要30秒以上的时间,比方说他们需要40秒。但是他们需要等待30秒钟过去,然后关闭灯以再次激活灯,然后他们可能在楼梯中间。因此,我们需要做的是允许在灯亮并且再次按下按钮时重置计时器。为此,我们将需要对系统进行重新建模,将计时器的开始建模为转换的输出,并添加另一个转换,如图2所示。

在这里,您可以看到两件事:

过渡完全可以进入到它来自的状态

过渡可以有一个事件作为发生过渡的条件,例如以及分配的输出。斜线左边的信息是事件,右边的信息是输出。事件也被视为机器的输入,这被称为输入-输出-自动机。

步骤2:在Arduino上手动实现FSM

Arduino

当我们想在Arduino上实现此行为时,代码可能类似于以下要点。代码没有什么特别的,switch-case语句仅针对每种可能的状态包含一个case,并在其中检查是否满足转换条件。如果是这样,状态就会更改。

如您所见,代码非常简单。但是您能想象如果没有2个州,而是10个或100个州,会发生什么情况?对于现实世界的FSM来说,这并不罕见。该代码变得不可读,并且可以达到数千行的长度。同样,通常,我们希望以图形方式计划FSM,因为我们需要能够尽快查看其实际功能。然后,我们仍然需要对实际的状态机进行编码,并且需要确保图形设计和手写代码实际上可以完成相同的工作。这可能是一个巨大的问题。

考虑一下:对于我们的FSM所具有的每个状态,我们的代码都需要一个“ case”语句,对于向其他状态的每个转换,我们都需要在其中包含一个if或case语句。如果我们有一个状态机,每个状态都可以到达其他每个状态(最极端的情况),我们的代码将以 n平方增长,其中 n 是状态。因此,对于3个状态,我们将有3种情况,内部有3个ifs,因此代码长度将与9成正比。当我们有10个状态(不是很多)时,代码长度将与100成正比,并且在20个州中,代码已长四倍。该FSM的图形表示将更容易掌握,并且如果我们不必处理所有这些switch case语句,那将很好。如果您熟悉描述模拟器原理图的网表–我们也不想使用网表设计原理图。那么,我们该怎么做呢?

步骤3:获取YAKINDU Statechart工具

Arduino

Yakindu SCT正是为此而设计的:对系统建模并从中生成代码。建模工具比简单的有限状态机先进得多,因为它们基于Harel的状态图理论。它们通过一些其他概念扩展了常规自动机理论-例如,历史状态,其中离开状态图可保存活动状态,因此您可以稍后再返回等等。对于‘Ible,我们将不需要这些额外的功能。

Yakindu SCT基于Eclipse(最常用的IDE之一)。因此,我们可以使用市场上所有的Eclipse插件,并拥有一个已知的环境。它是开源的,这意味着它是免费的!首先,请访问statecharts.org,然后选择“下载SCT”。您将需要输入姓名,电子邮件地址和职业。下载该工具后,只需解压缩它(右键单击-》全部提取,或类似操作)。在里面,您会发现“ SCT”。启动它。 (不,不需要真正的安装。)

在安装Yakindu SCT之后,您将具有对FSM进行建模的工具,但是我们将希望获得在Arduino上运行的代码。有一个出色的Eclipse插件可以做到这一点,您可以在http://www.baeyens.it/eclipse/上找到有关它的更多信息。它为您提供了Eclipse内部的完整Arduino工具链,因此您可以轻松使用Arduino IDE以及Eclipse的智能代码管理和编码助手。在SCT中,转到帮助-》安装新软件。安装向导打开。单击向导右上角附近的添加。.. 按钮。将打开一个对话框,要求您指定要从中安装新软件的更新存储库。在“名称”字段中输入一些文本。该文本原则上是任意的,但是您应该选择一些使其更容易在其他更新存储库中标识此特定更新存储库的内容。输入更新存储库的名称和位置(http://eclipse.baeyens.it/update/V4/stable)之后,单击“确定”。 Eclipse建立与更新存储库的网络连接,向其询问可用的软件项,并在安装向导中显示它们。在这里,您只需接受“ Arduino”选项。再单击几次“下一步”并在以后接受许可协议,它将要求您重新启动该工具。完成此操作后,插件将下载所有需要的库,因此您无需从现有的Arduino项目复制它们。接下来,在Yakindu SCT安装中安装了Arduino工具。现在是时候结合两者的可能性了。

注意:如果您尚未安装Windows,请同时安装官方的Arduino IDE。它带有必需的驱动程序。我不确定Mac上的情况。 Linux已经包含驱动程序,因此不需要安装Arduino IDE。

步骤4:开始创建状态图

Arduino

Arduino

Arduino

我们现在将开始一起对状态图进行建模。首先,我们将创建一个新项目。您应该在SCT/Eclipse的欢迎页面上。 转到文件-》新建-》项目。.. ,然后在主菜单中选择 Arduino-》新建Arduino Sketch 。将出现新Eclipse项目的常规向导。您必须给您的项目起一个名字。我们将其命名为ArduinoFSM。在下一个窗口中,您可以指定arduino连接到的端口。如果您不知道并且不知道如何查找,请忽略此。现在,您可以单击完成。

如果您改为选择 New-》 Arduino Sketch ,则不会询问您arduino的连接位置。然后,使用 Project-》 Properties 进行操作。如果您不知道如何确定Arduino的端口,此说明的最后一步将为您提供帮助。

如果在创建项目后未关闭欢迎屏幕,请关闭它您自己的,使用标签中的X。现在,您应该具有与左侧“项目资源管理器”中的第一张图片相似的图片。

我们现在要创建一个名为“ model”的新文件夹。右键单击您的项目,然后选择新建-》文件夹。键入名称,然后单击“完成”。

右键单击该新文件夹,再次转到“新建”。根据您的安装,您可能可以直接添加新的Statechart模型,或者可能必须使用Other,选择Yakindu,然后选择Statechart模型。现在,您应该看到的是第二张图片:一个进入状态和一个名为 StateA 的通用第一状态。

左侧的文本框允许您声明相关的事件和变量状态图,右边的区域是图形状态图编辑器。

我们将需要一个事件:按钮。双击左侧的文本框,然后在界面下插入文本

in event button

,然后声明有一个名为“ button”的传入事件。另外,在该文本框中双击单词“ default”,并给状态图取一个更好的名称-“ LightCtrl”怎么样?现在,添加另一个状态:只需在右侧面板中单击 State ,然后在图形状态图编辑器中的某个位置。双击这两个州的名称,并为其命名一个带有黑色输入状态的名称,熄灭,然后将新状态点亮。现在,我们需要过渡:从面板中选择过渡,单击一个状态,保持并拖动到另一状态。这应该构成过渡。它从您第一次单击的状态变为第二个状态。通过单击您现在拖动到第一个的状态并拖动到另一个状态来添加第二个过渡,这样您就可以在两个方向上进行过渡。现在,单击过渡。将出现一个文本字段。在这里,您可以输入要进行过渡的事件和输出。在从关闭灯光到打开灯光的过渡上,键入按钮,在另一个按钮上,在5秒后键入 (比测试的30秒要快)。现在,您应该拥有看起来像第三张图片的东西!

现在就这些了。您有一个楼梯灯的工作模型!

步骤5:模拟状态图

Arduino

Yakindu SCT的另一个不错的功能是您可以模拟状态图而无需事先编写任何代码。您可以尝试使用状态机来实现您想要的状态。

模拟状态图非常简单。右键单击Eclipse/SCT中的.sct文件,选择运行方式,然后选择状态图模拟。

将打开一个新透视图。您应该能够看到第一个状态是红色,这是活动状态。 (看图片)在右边,应该打开了Simulation View。您可以通过在右下方的模拟视图中单击单词 button 来模拟按钮按下事件。活动状态应从“熄灭”更改为“点亮”。五秒钟后,或单击时间事件 Light_On_timer_event_0 后,活动状态将更改回 Light Off 。太棒了!现在,让我们检查一下如何在Arduino上使用它。

步骤6:将系统带入现实世界

Arduino

Arduino

Arduino

Arduino

好吧,我们点击了一下,使用了图形编辑器(通常与低级语言相关联),让这件事栩栩如生。首先,我们需要一个代码生成器,将状态图转换为C代码。

右键单击您的模型文件夹,然后选择 New-》 Code Generator Model ,这非常简单,尽管看起来一开始就像是黑魔法。在向导中单击自己,然后将代码生成器附加到之前创建的状态图。注意:在同一窗口中,顶部有一个选择器,可以轻松查看。使用它选择C代码生成器而不是Java代码生成器,然后选中状态图旁边的复选框,然后单击完成。正常情况下,生成器现在应该一直一直直接自动运行。检查是否创建了两个文件夹src和src-gen。如果不是这种情况,请转到主菜单中的“项目”,然后检查是否激活了“自动生成”。如果不是,请这样做,然后右键单击您的项目,然后选择“生成项目”。进度条以及两个提到的文件夹都应出现。进行任何更改后,还可以右键单击生成器文件,然后选择 Generate Code Artifacts 。

src-gen文件夹的内容非常有趣。文件 LightCtrl.c 包含状态图的实现。检查时,您会发现一个函数 LightCtrlIface_raise_button(LightCtrl *句柄)。您可以调用此函数来引发我们先前声明的按钮事件,例如,当您检查硬件按钮的引脚并看到其具有高电平时。然后是文件 LightCtrlRequired.h ,您需要在其中查看。它声明您需要实现的功能。对于此状态图,只有两个功能: lightCtrl_setTimer 和 lightCtrl_unsetTimer 。我们需要这些功能,因为状态图在5s之后使用了构造。这是一个非常方便的功能,但是我们的状态图代码生成器不提供计时服务,因为它高度依赖于平台–您的计算机与微型Arduino的计时器处理方式不同,而Mac和Linux上的计时器处理方式与Windows上的处理方式不同。

幸运的是,我将为您提供计时服务,因此您无需自己实现。在您的项目中,创建一个新文件夹,将其命名为 scutils ,用于 s tate c hart 实用程序功能。您可以随意命名,也可以选择不创建该文件夹,这只是组织问题。我们将在其中创建两个文件,分别是 sc_timer_service.c 和 sc_timer_service.h 。从GitHub中复制

代码:

sc_timer_service.h

sc_timer_service.c

使用YAKINDU SCT 2.7.0,在那里是一个新选项,可用于获得此可指导的项目:

在SCT中,转到“文件”-》“新建”-》“示例。..”,选择“ YAKINDU Statechart示例”,然后单击“下一步”。在新的示例向导中,单击“下载”以获取最新的示例集。从arduino类别中选择“ Arduino的有限状态机”,然后单击“完成”。该项目将被复制到您的工作区中。右键单击它,然后单击“刷新”-可以肯定。

现在,我们可以开始在向导生成的* .ino文件中的Arduino代码上工作。

除了 Arduino.h ,还包括 avr/sleep.h ,当然还有我们的状态机和计时器服务: LightCtrl.h , LightCtrlRequired.h 和 sc_timer_service.h 。现在,需要常规的Arduino东西:我们定义按钮和LED的引脚,并将它们设置在设置功能内(这就是它的用途)。然后,我们需要定义状态图期望我们定义的函数-如前所述,- lightCtrl_setTimer 和 lightCtrl_unsetTimer 。在这里,我们只使用计时器服务,就完成了。现在,我们应该思考一下当达到 Light On 状态时实际上如何激活LED。基本上,我们有三个选项:

我们可以检查状态机是否处于Light On状态,并根据该信息激活/禁用LED

我们可以进入状态图,并在到达状态时设置一个变量,以便我们可以轮询

我们可以添加一个操作来管理状态图在过渡时调用的光。

第一个解决方案确实很糟糕。我们将有关于状态图外部的逻辑。如果我们重命名我们的州,它将停止正常工作;但是这些名称是平淡无奇的,与逻辑无关。可以使用变量,特别是在使用桌面应用程序时。我们可以每x毫秒左右与他们同步一次。在这里,我们要使用一个操作。在状态图的接口声明中添加以下内容:

operation setLight(LightOn: boolean): void

这声明了一个函数,该函数接受布尔值作为参数,但不返回任何值(无效)。这对您来说不是新手,只是这里的语法不同。请记住–状态图未绑定到特定语言,因此语法是通用的。此功能自动显示在 LightCtrlRequired.h 中。如果没有,请保存状态图,右键单击您的项目并进行构建。

此处声明的函数如下所示:

extern void lightCtrlIface_setLight(const LightCtrl* handle, const sc_boolean lightOn);

输入参数句柄为类型的LightCtrl,它是状态图的引用者。如果您不熟悉C:星号表示所谓的指针,那么该变量包含statechart变量的地址。这对我们有帮助,因为我们可以对原始对象进行操作,而不必创建其副本。因此,让我们实现此功能:

void lightCtrlIface_setLight(const LightCtrl* handle, const sc_boolean lightOn) {

if(lightOn)

digitalWrite(LED_PIN, HIGH);

else

digitalWrite(LED_PIN, LOW);

}

如您所见,此功能非常简单-我们甚至不使用状态图的句柄,我们只在LED上写HIGH如果操作的参数为true,则为pin;否则为LOW。

我们更改状态图本身,使其看起来像第一张图片。

还记得第1步吗?斜线左边是过渡所需要的输入,右边是状态机的输出(如果进行了过渡)。此处的输出是使用这些参数调用指定的操作。

#include “Arduino.h”

#include “avr/sleep.h”

#include “src-gen/LightCtrl.h”

#include “src-gen/LightCtrlRequired.h”

#include “scutil/sc_timer_service.h”

#define BUTTON_PIN 3

#define LED_PIN 6

#define MAX_TIMERS 20 //number of timers our timer service can use

#define CYCLE_PERIOD 10 //number of milliseconds that pass between each statechart cycle

static unsigned long cycle_count = 0L; //number of passed cycles

static unsigned long last_cycle_time = 0L; //timestamp of last cycle

static LightCtrl lightctrl;

static sc_timer_service_t timer_service;

static sc_timer_t timers[MAX_TIMERS];

//! callback implementation for the setting up time events

void lightCtrl_setTimer(LightCtrl* handle, const sc_eventid evid, const sc_integer time_ms, const sc_boolean periodic){

sc_timer_start(&timer_service, (void*) handle, evid, time_ms, periodic);

}

//! callback implementation for canceling time events.

void lightCtrl_unsetTimer(LightCtrl* handle, const sc_eventid evid) {

sc_timer_cancel(&timer_service, evid);

}

void lightCtrlIface_setLight(const LightCtrl* handle, const sc_boolean lightOn) {

if(lightOn)

digitalWrite(LED_PIN, HIGH);

else

digitalWrite(LED_PIN, LOW);

}

//The setup function is called once at startup of the sketch

void setup()

{

pinMode(BUTTON_PIN, INPUT);

pinMode(LED_PIN, OUTPUT);

sc_timer_service_init(

&timer_service,

timers,

MAX_TIMERS,

(sc_raise_time_event_fp) &lightCtrl_raiseTimeEvent

);

lightCtrl_init(&lightctrl); //initialize statechart

lightCtrl_enter(&lightctrl); //enter the statechart

}

// The loop function is called in an endless loop

void loop()

{

unsigned long current_millies = millis();

if(digitalRead(BUTTON_PIN))

lightCtrlIface_raise_button(&lightctrl);

if ( cycle_count == 0L || (current_millies 》= last_cycle_time + CYCLE_PERIOD) ) {

sc_timer_service_proceed(&timer_service, current_millies - last_cycle_time);

lightCtrl_runCycle(&lightctrl);

last_cycle_time = current_millies;

cycle_count++;

}

}

此外,请按行号在本要点中检查代码。

第1-6行包含如前所述的包含。

第8行和第9行定义了我们将要用于arduino的硬件引脚。

第11行和第12行定义了状态图可以使用多少个计时器,以及状态图的每个计算周期之间应经过多少毫秒。

第15和16行声明了一些变量,我们可以用它们来计数周期并管理最后一个周期的时间。

第17、19和21行声明了使用状态图的重要变量:状态图本身,计时器服务和计时器数组。

第24行和第33行定义了状态图需要计时器使用的功能,第33行是设置前面讨论过的LED的功能。

在第41行中,void setup()是Arduino的标准功能。它在启动时被调用一次。我们用它来初始化东西–我们的LED和按钮引脚配置了它们的方向(INPUT是标准的,为清楚起见,我们这样做了),计时器服务被初始化,状态图被初始化并输入。输入意味着启动状态机,因此第一个状态被激活-这是输入状态所指向的状态。因此,在启动时,指示灯熄灭。

在第59行中,跟随着循环功能,Arduino一直在调用它。

在第61行中,我们使用millis()函数捕获当前时间,该函数由Arduino库定义。

在第63行中,我们检查按钮是否被按下,如果按下,则引发按钮事件。

在第66行中,我们检查自上次循环状态图以来是否已超过CYCLE_PERIOD毫秒。

这会给我们的arduino带来一些负担,这意味着我们可以可靠地将长达10毫秒的时间用于自己的功能。

在第68行中,我们告诉计时器服务自上次调用以来已经过去了多少时间,在第70行中告诉statechart运行一个周期,在第72行中保存当前时间,并增加周期计数在第73行。

使用arduino插件,您现在可以将arduino与LED和连接到计算机的按钮连接起来,并使用顶部工具栏中的按钮将程序上传到

电路如图2和图3所示。

LED通过大约200欧姆的电阻连接到数字引脚(6)。阴极连接到GND。

按钮有四个引脚,请在按下按钮时检查其中哪些始终连接以及哪些连接。然后,将数字引脚(此处使用3)连接到一侧,将下拉电阻连接到GND。这将使引脚停止处于“浮动”状态(不确定状态),并将其保持在0 V电压。当按下按钮并将另一侧连接到VCC时,该侧“更强”,因为它没有电阻,并且电压高达5伏–基本上是一个分压器,其中一个电阻为0欧姆。请在此使用一个较高的电阻,因为它会限制通过按钮的电流。最小值为1 kR。

如您所见,该程序的逻辑完全独立于我们状态图的实际大小。状态图具有2个或20个状态都没有关系-当然,如果我们想做点什么,我们需要在这里和那里实现一个功能。但是void loop()内部的主要代码总是很小,并且允许模块化程序体系结构。我们只需要在代码中处理从状态图到Arduino硬件的接口,自动生成的状态图将处理其内部逻辑。还记得我们讨论过如何在再次按下按钮时重置计时器吗?现在,您可以使用“按钮”作为保护事件,从“点亮”状态添加到其自身的过渡,而无需在代码中更改或添加一行。尝试一下,然后开始对软件进行建模,而不是编写它!

步骤7:此外:查找您的Arduino端口

因此,您陷入困境,因为您无法弄清Arduino连接到哪个串行/USB端口。好的,您会在下面找到有关Windows和Linux的说明。

Windows

将arduino插入计算机,然后转到“设备和打印机”(从开始菜单或系统控制面板)。如图所示,您的arduino应该出现在这里-对我来说,端口为COM12。这可能会改变,例如,当您使用另一个USB端口时,重新启动系统。..如果仍然无法解决问题,请检查是否仍然正确。

Linux

使用您的arduino未连接,启动终端。输入 dmesg 并返回,这将为您提供冗长的文本输出。插入您的arduino,然后再次输入 dmesg 。最后应该是一些有关arduino的消息,包括一个端口-例如,/dev/USB0,/dev/ttyAMC3-可以理解。如果您插入arduino且LED不亮,并且 dmesg 在插入之前和之后都显示完全相同的内容,则很可能是您的Arduino吐司了。

如果此方法不适合您,也可以在插入Arduino之前和之后尝试 ls/dev/。这列出了所有可用的设备,并且在连接Arduino之后应该能够看到一个新设备。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分