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

奈何col 发表于 2022-9-4 03:11

状态机编程入门

本文将编入《Arduino程序设计基础(第3版)》,转载请注明出处

## 状态机编程

### 什么是状态机

有限状态机(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多次读取时间,判断时间间隔是否达标,再执行对应的程序。除此以外,还可以使用硬件本身具备的中断功能,对条件参数的进行实时的改变。

Highnose 发表于 2022-9-4 09:20

总算等来了老兄的这个贴子,给力,赞!

iggy 发表于 2022-9-5 11:34

本帖最后由 iggy 于 2022-9-6 07:38 编辑

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

int state = 0;

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

void turnOff()
{
digitalWrite(LED, LOW);
}

void SOS()
{
// S
digitalWrite(LED, HIGH);
delayMicroseconds(200000);
digitalWrite(LED, LOW);
delayMicroseconds(200000);

digitalWrite(LED, HIGH);
delayMicroseconds(200000);
digitalWrite(LED, LOW);
delayMicroseconds(200000);

digitalWrite(LED, HIGH);
delayMicroseconds(200000);
digitalWrite(LED, LOW);
delayMicroseconds(500000);

// O
digitalWrite(LED, HIGH);
delayMicroseconds(400000);
digitalWrite(LED, LOW);
delayMicroseconds(200000);

digitalWrite(LED, HIGH);
delayMicroseconds(400000);
digitalWrite(LED, LOW);
delayMicroseconds(200000);

digitalWrite(LED, HIGH);
delayMicroseconds(400000);
digitalWrite(LED, LOW);
delayMicroseconds(500000);

// S
digitalWrite(LED, HIGH);
delayMicroseconds(200000);
digitalWrite(LED, LOW);
delayMicroseconds(200000);

digitalWrite(LED, HIGH);
delayMicroseconds(200000);
digitalWrite(LED, LOW);
delayMicroseconds(200000);

digitalWrite(LED, HIGH);
delayMicroseconds(200000);
digitalWrite(LED, LOW);
delayMicroseconds(1300000);
}

void Strobe()
{
digitalWrite(LED, HIGH);
delayMicroseconds(20000);
digitalWrite(LED, LOW);
delayMicroseconds(5000000);
}

void setup()
{
pinMode(BUTTON, INPUT_PULLUP);
pinMode(LED, OUTPUT);
attachInterrupt(digitalPinToInterrupt(BUTTON), checkState, CHANGE);
}

void loop()
{
checkState();
switch (state)
{
case 0:
    Strobe();
    break;

case 1:
    SOS();
    break;

default:
    break;
}
}

奈何col 发表于 2022-9-5 22:38

iggy 发表于 2022-9-5 11:34
你好,我仿照帖子中的例程写了一个LED频闪程序,目前的问题是:按下并松开按键后,要等正在运行的动作函数 ...

请认真阅读上面内容,写了为什么不能用delay
页: [1]
查看完整版本: 状态机编程入门