[PICO][Adv]状态机

Raspberry Pi Pico 2 状态机设计

在嵌入式编程中,状态机(State Machine) 是一种强大的设计模式,用于管理复杂的行为和逻辑。状态机通过将程序分解为多个状态(State) 和状态之间的转换(Transition),使得代码更易于理解、维护和扩展。状态机特别适合处理需要根据输入或事件改变行为的任务,例如控制机器人、自动化系统或交互式设备。

本文将分别使用 Arduino 风格(基于 Arduino-Pico 核心)和 C/C++ 风格(基于官方 Pico SDK)来详细介绍状态机的设计与实现,并提供多个实际案例。


1. 什么是状态机?

有限状态机(Finite State Machine, FSM) 是一种数学模型,用于描述系统在不同状态之间的行为。它由以下几个核心部分组成:

组成部分 描述
状态(State) 系统在某一时刻的特定条件或模式,例如“LED 亮”、“LED 灭”
事件(Event) 触发状态转换的外部输入或内部条件,例如“按钮按下”、“计时结束”
转换(Transition) 从一个状态到另一个状态的变化规则
动作(Action) 在进入状态、离开状态或状态转换时执行的操作

状态机可以帮助你将复杂的逻辑分解为简单的状态和转换,从而:

  • 简化代码结构,提高可读性
  • 避免使用大量嵌套的 if-else 或标志变量
  • 更容易调试和扩展功能
  • 更符合硬件控制的思维模式

2. 状态机的基本结构

在 Pico 2 编程中,状态机通常通过 枚举类型 定义状态,并在主循环中使用 switch 语句 实现状态处理。

2.1 通用模板(Arduino 风格)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 1. 使用枚举定义所有可能的状态
enum State {
STATE_A,
STATE_B,
STATE_C
};

// 2. 定义当前状态变量,并设置初始状态
State currentState = STATE_A;

void setup() {
// 初始化硬件、引脚等
}

void loop() {
// 3. 根据当前状态执行对应的动作
switch (currentState) {
case STATE_A:
// 执行状态 A 的动作
// 检查是否需要转换到其他状态
if (/* 转换条件 */) {
currentState = STATE_B; // 状态转换
// 可选:执行进入状态 B 时的初始化动作
}
break;

case STATE_B:
// 执行状态 B 的动作
if (/* 转换条件 */) {
currentState = STATE_C;
}
break;

case STATE_C:
// 执行状态 C 的动作
if (/* 转换条件 */) {
currentState = STATE_A;
}
break;
}
}

2.2 通用模板(C/C++ 风格)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include "pico/stdlib.h"

// 使用枚举定义状态
typedef enum {
STATE_A,
STATE_B,
STATE_C
} State;

// 当前状态
State currentState = STATE_A;

int main() {
stdio_init_all();
// 初始化其他硬件...

while (true) {
switch (currentState) {
case STATE_A:
// 状态 A 的动作
if (/* 转换条件 */) {
currentState = STATE_B;
}
break;

case STATE_B:
// 状态 B 的动作
if (/* 转换条件 */) {
currentState = STATE_C;
}
break;

case STATE_C:
// 状态 C 的动作
if (/* 转换条件 */) {
currentState = STATE_A;
}
break;
}
sleep_ms(10); // 可选的小延时,避免循环过快
}
return 0;
}

核心思想:将系统的行为拆分为若干个离散的“状态”,每个状态下执行特定的动作,并在满足条件时切换到下一个状态。主循环只需不断查询当前状态并执行相应代码。


3. 实际应用案例

3.1 案例1:交通灯控制系统

设计一个简单的交通灯控制系统,包含红灯、黄灯和绿灯。状态转换规则:

  • 红灯:持续 5 秒 → 切换到绿灯
  • 绿灯:持续 5 秒 → 切换到黄灯
  • 黄灯:持续 2 秒 → 切换回红灯

硬件连接

  • 红灯:GP16
  • 黄灯:GP17
  • 绿灯:GP18

Arduino 风格实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// 定义交通灯状态
enum TrafficLightState {
RED,
YELLOW,
GREEN
};

TrafficLightState currentState = RED;
unsigned long lastStateChangeTime = 0; // 记录上次状态切换的时刻

// 引脚定义
const int RED_PIN = 16;
const int YELLOW_PIN = 17;
const int GREEN_PIN = 18;

void setup() {
pinMode(RED_PIN, OUTPUT);
pinMode(YELLOW_PIN, OUTPUT);
pinMode(GREEN_PIN, OUTPUT);

// 初始状态:红灯亮
digitalWrite(RED_PIN, HIGH);
digitalWrite(YELLOW_PIN, LOW);
digitalWrite(GREEN_PIN, LOW);

lastStateChangeTime = millis(); // 记录起始时间
}

void loop() {
unsigned long currentTime = millis();

switch (currentState) {
case RED:
// 检查是否达到持续时间(5秒)
if (currentTime - lastStateChangeTime >= 5000) {
// 切换到绿灯状态
currentState = GREEN;
lastStateChangeTime = currentTime;

// 更新引脚输出
digitalWrite(RED_PIN, LOW);
digitalWrite(GREEN_PIN, HIGH);
}
break;

case GREEN:
if (currentTime - lastStateChangeTime >= 5000) {
currentState = YELLOW;
lastStateChangeTime = currentTime;

digitalWrite(GREEN_PIN, LOW);
digitalWrite(YELLOW_PIN, HIGH);
}
break;

case YELLOW:
if (currentTime - lastStateChangeTime >= 2000) {
currentState = RED;
lastStateChangeTime = currentTime;

digitalWrite(YELLOW_PIN, LOW);
digitalWrite(RED_PIN, HIGH);
}
break;
}
}

C/C++ 风格实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include "pico/stdlib.h"
#include "hardware/timer.h"

// 定义状态枚举
typedef enum {
RED,
YELLOW,
GREEN
} TrafficLightState;

// 引脚定义
#define RED_PIN 16
#define YELLOW_PIN 17
#define GREEN_PIN 18

int main() {
stdio_init_all();

// 初始化引脚
gpio_init(RED_PIN);
gpio_init(YELLOW_PIN);
gpio_init(GREEN_PIN);
gpio_set_dir(RED_PIN, GPIO_OUT);
gpio_set_dir(YELLOW_PIN, GPIO_OUT);
gpio_set_dir(GREEN_PIN, GPIO_OUT);

TrafficLightState currentState = RED;
absolute_time_t lastStateChangeTime = get_absolute_time();

// 初始状态:红灯亮
gpio_put(RED_PIN, 1);
gpio_put(YELLOW_PIN, 0);
gpio_put(GREEN_PIN, 0);

while (true) {
absolute_time_t now = get_absolute_time();
uint64_t elapsed_us = absolute_time_diff_us(lastStateChangeTime, now);

switch (currentState) {
case RED:
// 持续 5 秒 (5,000,000 微秒)
if (elapsed_us >= 5000000) {
currentState = GREEN;
lastStateChangeTime = now;

gpio_put(RED_PIN, 0);
gpio_put(GREEN_PIN, 1);
}
break;

case GREEN:
if (elapsed_us >= 5000000) {
currentState = YELLOW;
lastStateChangeTime = now;

gpio_put(GREEN_PIN, 0);
gpio_put(YELLOW_PIN, 1);
}
break;

case YELLOW:
if (elapsed_us >= 2000000) {
currentState = RED;
lastStateChangeTime = now;

gpio_put(YELLOW_PIN, 0);
gpio_put(RED_PIN, 1);
}
break;
}
sleep_ms(10); // 避免过度占用 CPU
}
return 0;
}

关键点:使用 millis()(Arduino)或 get_absolute_time()(SDK)实现非阻塞延时,避免使用 delay() 阻塞整个程序。这是状态机设计中非常重要的技巧。


3.2 案例2:按钮控制的自动门系统

设计一个自动门控制系统,包含以下状态:

  • 关闭(CLOSED):门关闭,等待检测到人
  • 打开(OPEN):门完全打开,保持 5 秒后准备关闭
  • 关闭中(CLOSING):门正在关闭,若检测到人则重新打开,否则完全关闭后回到 CLOSED

硬件连接

  • 人体传感器(模拟 PIR 或按键模拟):GP0(输入,高电平表示检测到人)
  • 门位置传感器(模拟限位开关):GP1(输入,高电平表示门完全关闭)
  • 电机控制(模拟):GP2(输出,高电平表示电机正转开门,低电平表示电机反转关门)

Arduino 风格实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
enum DoorState {
CLOSED,
OPEN,
CLOSING
};

DoorState currentState = CLOSED;
unsigned long openStartTime = 0; // 记录门打开的时间
unsigned long closingStartTime = 0; // 记录开始关门的时间

const int SENSOR_PIN = 0; // 人体传感器
const int LIMIT_SWITCH_PIN = 1; // 门限位开关(关闭到位)
const int MOTOR_PIN = 2; // 电机控制

void setup() {
pinMode(SENSOR_PIN, INPUT);
pinMode(LIMIT_SWITCH_PIN, INPUT_PULLUP);
pinMode(MOTOR_PIN, OUTPUT);
digitalWrite(MOTOR_PIN, LOW); // 电机停止
Serial.begin(115200);
}

void loop() {
bool personDetected = digitalRead(SENSOR_PIN) == HIGH;
bool doorFullyClosed = digitalRead(LIMIT_SWITCH_PIN) == HIGH;
unsigned long now = millis();

switch (currentState) {
case CLOSED:
// 等待检测到人
if (personDetected) {
currentState = OPEN;
openStartTime = now;
digitalWrite(MOTOR_PIN, HIGH); // 电机正转,开门
Serial.println("Opening door...");
}
break;

case OPEN:
// 门已打开,保持 5 秒
if (now - openStartTime >= 5000) {
currentState = CLOSING;
closingStartTime = now;
digitalWrite(MOTOR_PIN, LOW); // 电机反转,关门
Serial.println("Closing door...");
}
break;

case CLOSING:
// 关门过程中,如果检测到人,立即重新开门
if (personDetected) {
currentState = OPEN;
openStartTime = now;
digitalWrite(MOTOR_PIN, HIGH); // 切换为开门
Serial.println("Person detected, reopening...");
}
// 如果门已完全关闭,回到 CLOSED 状态
else if (doorFullyClosed) {
currentState = CLOSED;
digitalWrite(MOTOR_PIN, LOW); // 停止电机
Serial.println("Door fully closed.");
}
break;
}
}

C/C++ 风格实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include "pico/stdlib.h"

typedef enum {
CLOSED,
OPEN,
CLOSING
} DoorState;

#define SENSOR_PIN 0
#define LIMIT_SWITCH_PIN 1
#define MOTOR_PIN 2

int main() {
stdio_init_all();

// 初始化引脚
gpio_init(SENSOR_PIN);
gpio_init(LIMIT_SWITCH_PIN);
gpio_init(MOTOR_PIN);
gpio_set_dir(SENSOR_PIN, GPIO_IN);
gpio_set_dir(LIMIT_SWITCH_PIN, GPIO_IN);
gpio_set_dir(MOTOR_PIN, GPIO_OUT);
gpio_pull_up(LIMIT_SWITCH_PIN); // 限位开关使用上拉

DoorState currentState = CLOSED;
absolute_time_t openStartTime;
absolute_time_t closingStartTime;
gpio_put(MOTOR_PIN, 0);

while (true) {
bool personDetected = gpio_get(SENSOR_PIN);
bool doorFullyClosed = gpio_get(LIMIT_SWITCH_PIN);
absolute_time_t now = get_absolute_time();

switch (currentState) {
case CLOSED:
if (personDetected) {
currentState = OPEN;
openStartTime = now;
gpio_put(MOTOR_PIN, 1);
printf("Opening door...\n");
}
break;

case OPEN: {
uint64_t elapsed = absolute_time_diff_us(openStartTime, now);
if (elapsed >= 5000000) { // 5秒
currentState = CLOSING;
closingStartTime = now;
gpio_put(MOTOR_PIN, 0);
printf("Closing door...\n");
}
break;
}

case CLOSING:
if (personDetected) {
currentState = OPEN;
openStartTime = now;
gpio_put(MOTOR_PIN, 1);
printf("Person detected, reopening...\n");
} else if (doorFullyClosed) {
currentState = CLOSED;
gpio_put(MOTOR_PIN, 0);
printf("Door fully closed.\n");
}
break;
}
sleep_ms(10);
}
return 0;
}

设计要点

  • 使用 millis() / get_absolute_time() 实现非阻塞定时,避免程序在等待时无法响应事件。
  • CLOSING 状态中,同时检查“人是否还在”和“门是否已关到位”,体现了状态机处理并发条件的能力。

3.3 案例3:长按与短按识别(状态机进阶)

使用状态机识别按钮的短按和长按,执行不同动作。这是一个比较经典的状态机应用。

状态定义

  • IDLE:等待按钮按下
  • PRESS_DETECTED:检测到按钮按下,启动计时
  • SHORT_PRESS:按钮松开且时间短于阈值,执行短按动作后回到 IDLE
  • LONG_PRESS:按钮按下时间超过长按阈值,执行长按动作后等待松开

Arduino 风格实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
enum ButtonState {
IDLE,
PRESS_DETECTED,
SHORT_PRESS,
LONG_PRESS
};

ButtonState state = IDLE;
unsigned long pressStartTime = 0;

const int BUTTON_PIN = 0;
const int LED_PIN = 25; // 板载 LED

// 阈值定义
const unsigned long LONG_PRESS_TIME = 1000; // 1秒为长按
const unsigned long DEBOUNCE_TIME = 50; // 防抖时间

void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
Serial.begin(115200);
}

void loop() {
bool buttonPressed = (digitalRead(BUTTON_PIN) == LOW); // 上拉电阻,按下为 LOW
unsigned long now = millis();

switch (state) {
case IDLE:
if (buttonPressed) {
// 进入按下检测状态
state = PRESS_DETECTED;
pressStartTime = now;
}
break;

case PRESS_DETECTED:
if (!buttonPressed) {
// 按钮松开,且时间小于长按阈值 -> 短按
if (now - pressStartTime < LONG_PRESS_TIME) {
state = SHORT_PRESS;
} else {
// 如果超过阈值且之前未进入长按,则先进入 IDLE(一般不会发生)
state = IDLE;
}
}
else if (now - pressStartTime >= LONG_PRESS_TIME) {
// 按下时间达到长按阈值 -> 长按
state = LONG_PRESS;
Serial.println("Long press detected!");
digitalWrite(LED_PIN, HIGH); // 长按点亮 LED
}
break;

case SHORT_PRESS:
// 执行短按动作:LED 闪烁一次
digitalWrite(LED_PIN, HIGH);
delay(100);
digitalWrite(LED_PIN, LOW);
Serial.println("Short press detected!");
state = IDLE;
break;

case LONG_PRESS:
// 等待按钮松开
if (!buttonPressed) {
state = IDLE;
// 长按动作已在进入状态时执行,此处可做清理
}
break;
}
}

C/C++ 风格实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include "pico/stdlib.h"

typedef enum {
IDLE,
PRESS_DETECTED,
SHORT_PRESS,
LONG_PRESS
} ButtonState;

#define BUTTON_PIN 0
#define LED_PIN 25
#define LONG_PRESS_TIME_MS 1000
#define DEBOUNCE_TIME_MS 50

int main() {
stdio_init_all();
gpio_init(BUTTON_PIN);
gpio_init(LED_PIN);
gpio_set_dir(BUTTON_PIN, GPIO_IN);
gpio_set_dir(LED_PIN, GPIO_OUT);
gpio_pull_up(BUTTON_PIN);

ButtonState state = IDLE;
absolute_time_t pressStartTime;

while (true) {
bool buttonPressed = (gpio_get(BUTTON_PIN) == 0);
absolute_time_t now = get_absolute_time();

switch (state) {
case IDLE:
if (buttonPressed) {
state = PRESS_DETECTED;
pressStartTime = now;
}
break;

case PRESS_DETECTED: {
uint64_t elapsed_ms = absolute_time_diff_us(pressStartTime, now) / 1000;
if (!buttonPressed) {
if (elapsed_ms < LONG_PRESS_TIME_MS) {
state = SHORT_PRESS;
} else {
state = IDLE;
}
} else if (elapsed_ms >= LONG_PRESS_TIME_MS) {
state = LONG_PRESS;
printf("Long press detected!\n");
gpio_put(LED_PIN, 1);
}
break;
}

case SHORT_PRESS:
gpio_put(LED_PIN, 1);
sleep_ms(100);
gpio_put(LED_PIN, 0);
printf("Short press detected!\n");
state = IDLE;
break;

case LONG_PRESS:
if (!buttonPressed) {
state = IDLE;
gpio_put(LED_PIN, 0);
}
break;
}
sleep_ms(10);
}
return 0;
}

进阶技巧:本例展示了状态机处理“时间相关”事件的强大能力。通过状态 PRESS_DETECTED 作为中间态,可以精确区分短按和长按,且代码结构清晰,易于扩展(如添加双击识别)。


4. 状态机设计要点与常见模式

4.1 非阻塞设计

状态机的核心优势之一就是避免使用 delay()。所有定时都应通过记录起始时间并检查时间差来实现,这样主循环可以持续运行,及时响应事件。

4.2 状态转换图

在编写代码前,建议先画出状态转换图,明确:

  • 有哪些状态
  • 每个状态下执行什么动作
  • 什么事件会导致状态转换

例如交通灯的状态转换图:

1
红灯 ──(5秒)──> 绿灯 ──(5秒)──> 黄灯 ──(2秒)──> 红灯

4.3 进入/退出动作

有时需要在进入某个状态时执行初始化操作,或在离开时执行清理。可以在状态切换的代码位置直接添加这些动作,例如:

1
2
3
4
5
6
7
8
9
10
11
case STATE_A:
if (condition) {
// 离开 STATE_A 前的退出动作
doExitActionA();

currentState = STATE_B;

// 进入 STATE_B 时的进入动作
doEntryActionB();
}
break;

4.4 层次化状态机

当系统非常复杂时,可以将状态机嵌套(一个状态内部又是一个子状态机)。例如自动门系统中,“OPEN”状态内部可能包含“正在开门”和“门已开”两个子状态。但通常建议先保持扁平化,直到复杂度确实需要时才引入层次化。

4.5 状态机的优缺点

优点 缺点
逻辑清晰,易于理解和调试 状态较多时 switch 代码会变长
避免大量标志变量和嵌套 if 需要一定的设计规划
易于扩展新的状态 对初学者有理解门槛
非常适合事件驱动系统 处理并发事件需要仔细设计

5. 总结

状态机是一种强大的设计模式,特别适合处理复杂的逻辑和行为。通过将程序分解为多个状态和转换,你可以编写出更高效、可维护的 Pico 2 代码。本文介绍了:

  • 状态机的基本概念(状态、事件、转换、动作)
  • 通用代码模板(Arduino 风格和 C/C++ 风格)
  • 三个实际案例:交通灯控制系统、自动门系统、长按短按识别
  • 设计要点:非阻塞设计、状态转换图、进入/退出动作等
案例 核心知识点
交通灯 时间触发的状态转换
自动门 事件触发与状态中的条件监测
长按/短按 时间测量与中间状态设计

6. 练习与拓展

  • 练习 1:为交通灯系统增加一个“夜间模式”状态(例如黄灯闪烁),通过外部开关触发状态切换。
  • 练习 2:设计一个简单的自动贩卖机状态机(等待投币 → 选择商品 → 出货 → 找零 → 回到等待),并用串口模拟输入。
  • 练习 3:将自动门案例中的电机控制替换为真实的舵机或步进电机,并增加障碍物检测传感器(如超声波)实现防夹功能。
  • 练习 4:结合之前的条件语句知识,将状态机与 switch-case 结合,实现一个基于菜单的交互系统。

通过学习和实践状态机设计,你将能够更好地管理复杂的 Pico 2 项目,并提升代码质量与可维护性。


[PICO][Adv]状态机
https://ka5fxt.cn/2026/03/30/PICO-Adv-状态机设计/
发布于
2026年3月30日
许可协议