有限状态机(FSM)基础:状态、事件、转移、动作

各位同学,今天我们来聊聊自动驾驶决策系统里最经典、也最基础的一个工具——有限状态机。说实话,我入行那会儿,第一个接触的决策算法就是FSM。那时候觉得这东西太简单了,不就是几个状态来回跳吗?后来在实车上跑起来才发现,越是简单的东西,越容易在细节上翻车。

先给个定义。有限状态机,说白了就是一个数学模型。它描述了一个系统在任意时刻只能处于有限个状态中的一个。系统根据外部事件,从一个状态转移到另一个状态,同时执行一些动作。嗯,就这么简单。

状态、事件、转移、动作的定义

这四个要素是FSM的基石。我一个个说。

状态(State):系统在某一时刻的稳定情形。比如车辆处于「停车」、「启动」、「巡航」这三种状态。注意,状态必须是互斥的——你不可能同时既停车又巡航。我在项目中见过有人把状态定义得重叠了,结果逻辑跑起来乱成一锅粥。

事件(Event):触发状态转移的外部或内部信号。比如「刹车踏板踩下」、「车速超过阈值」、「前方障碍物消失」。事件是转移的导火索。

转移(Transition):从一个状态到另一个状态的路径。转移通常由事件触发,并且可以附带条件。比如「在巡航状态下,如果检测到前车减速且距离小于安全阈值,则转移到减速跟车状态」。

动作(Action):进入状态、退出状态或执行转移时触发的行为。比如进入「启动」状态时,执行「松开刹车、轻踩油门」的动作。动作可以分三类:进入动作、退出动作、转移动作。

核心要点:状态是「在哪里」,事件是「发生了什么」,转移是「怎么过去」,动作是「过去时做什么」。这四个东西缺一不可。

FSM的数学表示

从数学角度看,FSM可以用一个六元组来表示:

M = (S, E, T, s₀, A, δ)

其中:

  • S:有限状态集合,比如 {停车, 启动, 巡航}
  • E:有限事件集合,比如 {刹车信号, 油门信号, 车速达标}
  • T:转移函数,T: S × E → S,给定当前状态和事件,决定下一个状态
  • s₀:初始状态,车辆上电后默认进入的状态
  • A:动作集合,每个状态或转移可以关联一个动作
  • δ:输出函数,描述动作如何被触发

你可能会问,为什么要用数学表示?直接写代码不就行了?我个人的习惯是,先画状态转移图,再写数学定义,最后才写代码。数学表示能帮你发现遗漏的状态和事件。有一次我在项目里漏了一个「紧急制动」事件,结果仿真时车辆在巡航状态下遇到行人直接懵了——因为没有定义这个事件对应的转移。数学表示一列出来,问题一目了然。

手写一个简单的车辆状态机

好了,理论说完了,咱们直接上手写代码。下面是一个最简单的车辆状态机,包含三个状态:停车、启动、巡航。

# 定义状态枚举
from enum import Enum

class VehicleState(Enum):
    PARKED = 1      # 停车
    STARTING = 2    # 启动
    CRUISING = 3    # 巡航

# 定义事件枚举
class VehicleEvent(Enum):
    START_BUTTON = 1      # 按下启动按钮
    SPEED_READY = 2       # 车速达到巡航条件
    BRAKE_PRESSED = 3     # 刹车被踩下
    STOP_SIGNAL = 4       # 停车信号

# 简单的FSM类
class VehicleFSM:
    def __init__(self):
        self.state = VehicleState.PARKED
        self.speed = 0.0
        print(f"初始状态: {self.state.name}")

    def on_event(self, event):
        # 状态转移逻辑
        if self.state == VehicleState.PARKED:
            if event == VehicleEvent.START_BUTTON:
                self.state = VehicleState.STARTING
                self._enter_starting()
            else:
                print(f"停车状态下忽略事件: {event.name}")

        elif self.state == VehicleState.STARTING:
            if event == VehicleEvent.SPEED_READY:
                self.state = VehicleState.CRUISING
                self._enter_cruising()
            elif event == VehicleEvent.BRAKE_PRESSED:
                self.state = VehicleState.PARKED
                self._enter_parked()
            else:
                print(f"启动状态下忽略事件: {event.name}")

        elif self.state == VehicleState.CRUISING:
            if event == VehicleEvent.BRAKE_PRESSED:
                self.state = VehicleState.STARTING
                self._enter_starting()
            elif event == VehicleEvent.STOP_SIGNAL:
                self.state = VehicleState.PARKED
                self._enter_parked()
            else:
                print(f"巡航状态下忽略事件: {event.name}")

    def _enter_parked(self):
        self.speed = 0.0
        print("进入停车状态,车速归零,拉起手刹")

    def _enter_starting(self):
        self.speed = 5.0
        print("进入启动状态,松开刹车,轻踩油门,车速缓慢提升")

    def _enter_cruising(self):
        self.speed = 60.0
        print("进入巡航状态,保持匀速行驶")

# 模拟运行
if __name__ == "__main__":
    fsm = VehicleFSM()
    fsm.on_event(VehicleEvent.START_BUTTON)   # 停车 → 启动
    fsm.on_event(VehicleEvent.SPEED_READY)    # 启动 → 巡航
    fsm.on_event(VehicleEvent.BRAKE_PRESSED)  # 巡航 → 启动
    fsm.on_event(VehicleEvent.STOP_SIGNAL)    # 启动 → 停车

个人经验:写FSM时,我建议把每个状态的进入动作和退出动作分开定义。比如进入「启动」状态时执行「松开刹车」,退出「启动」状态时执行「关闭启动指示灯」。这样逻辑清晰,后期维护也方便。我曾经在一个项目里把所有动作都塞在转移函数里,结果三个月后自己都看不懂了。

状态转移表

为了更直观地展示状态机逻辑,我习惯画一个状态转移表。你看,这样一目了然:

当前状态 事件 下一状态 执行动作
停车 启动按钮 启动 松开刹车,轻踩油门
启动 车速达标 巡航 保持匀速
启动 刹车 停车 减速至零,拉手刹
巡航 刹车 启动 减速,准备重新加速
巡航 停车信号 停车 减速至零,拉手刹

避坑指南:我曾经在项目里犯过一个低级错误——没有处理「非法事件」。比如在停车状态下收到「车速达标」事件,代码直接崩溃了。后来我养成了一个习惯:每个状态都要有一个「默认处理」,要么忽略事件,要么报错。千万别让未定义的事件穿透到下一层逻辑。

FSM的优缺点

说实话,FSM不是万能的。它有明显的优点,也有硬伤。

优点

  • 直观易懂,画个图就能讲清楚
  • 实现简单,代码量小
  • 确定性高,给定状态和事件,结果唯一

缺点

  • 状态爆炸:状态一多,转移关系呈指数增长
  • 难以处理并发事件:两个事件同时来了怎么办?
  • 扩展性差:加一个新状态,可能要改一堆转移逻辑

你想想看,如果一辆自动驾驶汽车有几十个状态,每个状态又有十几个事件,那状态转移图画出来跟蜘蛛网一样。这时候FSM就不够用了。后面我们会讲层次状态机(HSM)和行为树,它们能解决这些问题。

嗯,今天的内容就到这里。记住,FSM是基础,但基础不牢,地动山摇。下一章我们讲层次状态机,看看怎么用嵌套的方式解决状态爆炸问题。