状态机编程入门-Arduino中文社区 - Powered by Discuz!

Arduino中文社区

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 828|回复: 3

[拓展] 状态机编程入门

[复制链接]
发表于 2022-9-4 03:11 | 显示全部楼层 |阅读模式
本文将编入《Arduino程序设计基础(第3版)》,转载请注明出处

[md]## 状态机编程  

### 什么是状态机

有限状态机(finite state machine,简称状态机)是嵌入式开发中最重要,且最常用的编程模式之一。其设定了这样一种系统:

1. 系统具备数量有限的状态;

2. 在某一时刻,系统总是处于一种状态之下;

3. 系统的状态会根据某些事件或条件进行转换;

4. 每个状态下,系统都会执行相应的动作。




状态机的编程思想在之前的章节也有体现,如开关灯示例:

```
int buttonPin = 2;
int ledPin = 13;
boolean ledState=false;  // 记录LED状态

void setup()
{
  // 初始化I/O口
  pinMode(buttonPin, INPUT_PULLUP);
  pinMode(ledPin,OUTPUT);
}

void loop()
{
  // 等待按键按下
  while(digitalRead(buttonPin)==HIGH){}

  // 当按键按下时,点亮或熄灭LED
  if(ledState==true)
  {
    digitalWrite(ledPin,LOW);
    ledState=!ledState;
  }
  else
  {
    digitalWrite(ledPin,HIGH);
    ledState=!ledState;
  }
  delay(500);
}
```

在这个示例中,设备具备开和关两种状态,使用了一个布尔变量ledState存放当前状态,会根据当前状态进行对应的开关灯操作。

现在请思考,当设备具备更多状态时,应该如何扩展这个程序?如何让灯可以进入闪烁或呼吸的状态?

以现有的这个程序写法来说,很难,因为程序大部分时间都在等待按键状态的变化,处于` while(digitalRead(buttonPin)==HIGH)`这个循环中。

现在将以状态机的编程模式,重构该项目。在此过程中,即可学习到状态机编程的方法,体会到使用该模式的优势。



### 状态机编程方法

状态机编程基本分为如下几个步骤:  

**1. 总结设备状态**  

本项目中,设备具备两种状态,开灯状态和关灯状态。

为了之后能支持更多的状态,这里用一个int变量state记录其状态:  

```
int state = 0; // 0:关灯状态   1:开灯状态
```



**2. 确定状态切换条件**   
设备连接的按键每点击一次,就会切换一次开关状态,通过循环检测按键状态变化(按下按键到松开,电平从低电平变为高电平),确定按键是否被点击。这里创建一个状态检查函数`checkState`,将其放在loop循环中,不断地检测当前设备处于的状态。  

```
int state = 0; // 0:关灯状态   1:开灯状态

boolean buttonLastState = HIGH;

void loop()
{
    checkState();
}

void checkState()
{
    boolean buttonCurrentState = digitalRead(buttonPin);
    if (buttonLastState == LOW && buttonCurrentState == HIGH)
    {
        state++;
        if (state > 1)
        {
            state = 0;
        }
    }
    buttonLastState = buttonCurrentState;
}
```



**3. 确定状态对应的动作**  
当设备会执行的动作有二:开灯和关灯,这里编写出对应的动作函数  

```
// 开灯动作
void turnOn()
{
    digitalWrite(ledPin, HIGH);
}

// 关灯动作
void turnOff()
{
    digitalWrite(ledPin, LOW);
}
```



**4. 判断当前状态,并执行相应的动作:**  
有了状态检查和对应的动作函数后,通过`switch`语句将两者联系起来,从而实现设备根据状态,执行对应的动作的功能。  

```
void loop()
{
    checkState();
    switch (state)
    {
    case 0:
        turnOff();
        break;
    case 1:
        turnOn();
        break;
    default:
        break;
    }
}
```



使用状态机编程的完整开关灯程序如下:  

```
int state = 0; // 0:关灯状态   1:开灯状态

int buttonPin = 8;
int ledPin = 9;

// 状态检查
boolean buttonLastState = HIGH;
void checkState()
{
    boolean buttonCurrentState = digitalRead(buttonPin);
    if (buttonLastState == LOW && buttonCurrentState == HIGH)
    {
        state++;
        if (state > 1)
        {
            state = 0;
        }
    }
    buttonLastState = buttonCurrentState;
}

// 开灯动作
void turnOn()
{
    digitalWrite(ledPin, HIGH);
}

// 关灯动作
void turnOff()
{
    digitalWrite(ledPin, LOW);
}

void setup()
{
    pinMode(buttonPin, INPUT_PULLUP);
    pinMode(ledPin, OUTPUT);
    Serial.begin(115200);
}

void loop()
{
    checkState();
    switch (state)
    {
    case 0:
        turnOff();
        break;
    case 1:
        turnOn();
        break;
    default:
        break;
    }
}
```



### 扩展更多状态

状态机编程的最大优势,是能让程序逻辑更清晰,逻辑清晰也使得程序,具备了更好的扩展性。



例如,在这个框架基础上,给设备增加闪烁和呼吸两种状态:闪烁状态下,灯每隔1秒,切换一次开关状态;呼吸状态下,会呈现出呼吸灯效果。
增加状态后的完整程序如下:  

```
int state = 0; // 0:关灯状态   1:开灯状态   2:闪烁状态  3:呼吸状态

int buttonPin = 8;
int ledPin = 9;

void setup()
{
    pinMode(buttonPin, INPUT_PULLUP);
    pinMode(ledPin, OUTPUT);
    Serial.begin(115200);
}

void loop()
{
    checkState();
    switch (state)
    {
    case 0:
        turnOff();
        break;
    case 1:
        turnOn();
        break;
    case 2:
        blink();
        break;
    case 3:
        fade();
        break;
    default:
        break;
    }
}

// 状态检查
boolean buttonLastState = HIGH;
void checkState()
{
    boolean buttonCurrentState = digitalRead(buttonPin);
    if (buttonLastState == LOW && buttonCurrentState == HIGH)
    {
        state++;
        if (state > 3)
        {
            state = 0;
        }
    }
    buttonLastState = buttonCurrentState;
}

// 开灯动作
void turnOn()
{
    digitalWrite(ledPin, HIGH);
}

// 关灯动作
void turnOff()
{
    digitalWrite(ledPin, LOW);
}

// 闪烁动作
unsigned long lastChangeTime=0;
void blink()
{
    if(millis()-lastChangeTime>1000){
     digitalWrite(ledPin, !digitalRead(ledPin));
     lastChangeTime = millis();
    }
}

// 呼吸动作
int brightness=0;
int fadeAmount=1;
void fade()
{
  if(millis()-lastChangeTime>10){
    analogWrite(ledPin, brightness);
    Serial.println(brightness);
    brightness = brightness + fadeAmount;
    if (brightness <= 0 || brightness >= 255)
    {
         fadeAmount = -fadeAmount;
    }
    lastChangeTime = millis();
  }
}

```

本示例中通过`checkState`函数获取当前设备的状态变化,然后通过`switch`语句判断设备状态,并执行对应的动作。   

**状态机编程注意事项**  

在基本掌握了状态机编程方法后,还应该了解如下事项:

状态机编程应该尽可能的提高MCU的使用效率,让系统能更积极的响应外部输入的变化,并即时做出相应的动作。

大部分MCU都不具备多个核心,其同一时刻,只能执行一条指令。如果使用delay进行延时,或使用之前例程中的`while(digitalRead(buttonPin)==HIGH)`一类的判断方式,将导致整个程序阻塞,影响此后的其他操作。

因此在状态机程序中,需避免进行耗时较长的操作,常见的解决办法是,通过millis多次读取时间,判断时间间隔是否达标,再执行对应的程序。除此以外,还可以使用硬件本身具备的中断功能,对条件参数的进行实时的改变。[/md]
发表于 2022-9-4 09:20 | 显示全部楼层
总算等来了老兄的这个贴子,给力,赞!
发表于 2022-9-5 11:34 | 显示全部楼层
本帖最后由 iggy 于 2022-9-6 07:38 编辑

你好,我仿照帖子中的例程写了一个LED频闪程序,目前的问题是:按下并松开按键后,要等正在运行的动作函数运行完毕才能切换到另一个动作函数,写了中断貌似也不起作用,请问如何修改?
  1. #include "Arduino.h"
  2. #define LED 0
  3. #define BUTTON 1

  4. int state = 0;

  5. boolean buttonLastState = HIGH;
  6. void checkState()
  7. {
  8.   boolean buttonCurrentState = digitalRead(BUTTON);
  9.   if (buttonLastState == LOW && buttonCurrentState == HIGH)
  10.   {
  11.     state++;
  12.     if (state > 1)
  13.     {
  14.       state = 0;
  15.     }
  16.   }
  17.   buttonLastState = buttonCurrentState;
  18. }

  19. void turnOff()
  20. {
  21.   digitalWrite(LED, LOW);
  22. }

  23. void SOS()
  24. {
  25.   // S
  26.   digitalWrite(LED, HIGH);
  27.   delayMicroseconds(200000);
  28.   digitalWrite(LED, LOW);
  29.   delayMicroseconds(200000);

  30.   digitalWrite(LED, HIGH);
  31.   delayMicroseconds(200000);
  32.   digitalWrite(LED, LOW);
  33.   delayMicroseconds(200000);

  34.   digitalWrite(LED, HIGH);
  35.   delayMicroseconds(200000);
  36.   digitalWrite(LED, LOW);
  37.   delayMicroseconds(500000);

  38.   // O
  39.   digitalWrite(LED, HIGH);
  40.   delayMicroseconds(400000);
  41.   digitalWrite(LED, LOW);
  42.   delayMicroseconds(200000);

  43.   digitalWrite(LED, HIGH);
  44.   delayMicroseconds(400000);
  45.   digitalWrite(LED, LOW);
  46.   delayMicroseconds(200000);

  47.   digitalWrite(LED, HIGH);
  48.   delayMicroseconds(400000);
  49.   digitalWrite(LED, LOW);
  50.   delayMicroseconds(500000);

  51.   // S
  52.   digitalWrite(LED, HIGH);
  53.   delayMicroseconds(200000);
  54.   digitalWrite(LED, LOW);
  55.   delayMicroseconds(200000);

  56.   digitalWrite(LED, HIGH);
  57.   delayMicroseconds(200000);
  58.   digitalWrite(LED, LOW);
  59.   delayMicroseconds(200000);

  60.   digitalWrite(LED, HIGH);
  61.   delayMicroseconds(200000);
  62.   digitalWrite(LED, LOW);
  63.   delayMicroseconds(1300000);
  64. }

  65. void Strobe()
  66. {
  67.   digitalWrite(LED, HIGH);
  68.   delayMicroseconds(20000);
  69.   digitalWrite(LED, LOW);
  70.   delayMicroseconds(5000000);
  71. }

  72. void setup()
  73. {
  74.   pinMode(BUTTON, INPUT_PULLUP);
  75.   pinMode(LED, OUTPUT);
  76.   attachInterrupt(digitalPinToInterrupt(BUTTON), checkState, CHANGE);
  77. }

  78. void loop()
  79. {
  80.   checkState();
  81.   switch (state)
  82.   {
  83.   case 0:
  84.     Strobe();
  85.     break;

  86.   case 1:
  87.     SOS();
  88.     break;

  89.   default:
  90.     break;
  91.   }
  92. }
复制代码


点评

请认真阅读上面内容,写了为什么不能用delay  详情 回复 发表于 2022-9-5 22:38
 楼主| 发表于 2022-9-5 22:38 | 显示全部楼层
iggy 发表于 2022-9-5 11:34
你好,我仿照帖子中的例程写了一个LED频闪程序,目前的问题是:按下并松开按键后,要等正在运行的动作函数 ...

请认真阅读上面内容,写了为什么不能用delay
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|Archiver|手机版|Arduino中文社区

GMT+8, 2024-11-28 03:36 , Processed in 0.081240 second(s), 19 queries .

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表