状态机编程入门
本文将编入《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多次读取时间,判断时间间隔是否达标,再执行对应的程序。除此以外,还可以使用硬件本身具备的中断功能,对条件参数的进行实时的改变。 总算等来了老兄的这个贴子,给力,赞! 本帖最后由 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;
}
}
iggy 发表于 2022-9-5 11:34
你好,我仿照帖子中的例程写了一个LED频闪程序,目前的问题是:按下并松开按键后,要等正在运行的动作函数 ...
请认真阅读上面内容,写了为什么不能用delay
页:
[1]