1. 有限状态机基础概念

有限状态机(Finite State Machine,简称FSM)是计算机科学中一个非常重要的数学模型。简单来说,它描述了一个系统在不同状态之间的转换过程。想象一下你家的电风扇:它有"关闭"、"低速"、"中速"和"高速"几个档位,每个档位就是一个状态,而你按下的调速按钮就是触发状态转换的事件。

在实际工程中,FSM主要由四个核心要素组成:

  • 状态(State):系统所处的特定模式或条件。比如自动驾驶汽车可能有"直行"、"变道"、"停车"等状态。
  • 转移(Transition):从一个状态切换到另一个状态的过程。
  • 事件(Event):触发状态转移的条件或输入信号。比如交通灯变红就是一个事件。
  • 动作(Action):在状态转移时或进入某个状态时执行的操作。

我见过很多新手容易混淆状态和事件的概念。这里有个简单的区分技巧:状态通常是名词(如"停车状态"),而事件通常是动词(如"刹车信号")。在实际编码时,这种命名习惯也能帮你保持清晰的思路。

2. 状态机的三种实现方式

2.1 分支逻辑法

分支逻辑法是最直观的实现方式,适合状态数量少、逻辑简单的场景。它的核心就是用一堆if-else或者switch-case语句来处理状态转换。比如下面这个简单的红绿灯状态机:

enum TrafficLightState { RED, YELLOW, GREEN };
enum TrafficLightEvent { TIMER_EXPIRED, EMERGENCY };

class TrafficLightFSM {
    TrafficLightState currentState = RED;
    
public:
    void handleEvent(TrafficLightEvent event) {
        switch(currentState) {
            case RED:
                if(event == TIMER_EXPIRED) {
                    currentState = GREEN;
                    startTimer(60); // 绿灯持续60秒
                }
                break;
            case GREEN:
                if(event == TIMER_EXPIRED) {
                    currentState = YELLOW;
                    startTimer(5); // 黄灯5秒
                }
                break;
            case YELLOW:
                if(event == TIMER_EXPIRED) {
                    currentState = RED;
                    startTimer(30); // 红灯30秒
                }
                break;
        }
    }
};

这种方法的优点是简单直接,但缺点也很明显:当状态和事件增多时,代码会变得难以维护。我曾经在一个项目中使用这种方法实现了一个有15个状态的系统,结果代码变成了"面条式"的混乱结构,后来不得不重构。

2.2 查表法

查表法更适合状态多但转换逻辑相对固定的场景。它的核心思想是用二维表来表示状态转换规则。表格的行代表当前状态,列代表事件,单元格内填写的是下一个状态。

// 状态枚举
enum ElevatorState { STOPPED, MOVING_UP, MOVING_DOWN, DOOR_OPEN };

// 事件枚举
enum ElevatorEvent { FLOOR_REACHED, BUTTON_PRESSED, DOOR_TIMEOUT };

// 状态转换表
ElevatorState transitionTable[4][3] = {
    /* STOPPED */     { STOPPED,    MOVING_UP,   DOOR_OPEN },
    /* MOVING_UP */   { STOPPED,    MOVING_UP,   MOVING_UP },
    /* MOVING_DOWN */ { STOPPED,    MOVING_DOWN, MOVING_DOWN },
    /* DOOR_OPEN */   { DOOR_OPEN,  STOPPED,     STOPPED }
};

class ElevatorFSM {
    ElevatorState currentState = STOPPED;
    
public:
    void handleEvent(ElevatorEvent event) {
        currentState = transitionTable[currentState][event];
    }
};

查表法的优点是状态转换规则一目了然,添加新状态也相对容易。但缺点是难以处理需要复杂条件判断的转换,而且动作执行逻辑通常需要额外的处理。

2.3 状态模式

状态模式是面向对象的方式实现FSM,每个状态都是一个独立的类。这种方法最适合状态不多但每个状态的逻辑比较复杂的情况。

// 状态接口
class ElevatorState {
public:
    virtual void handleEvent(ElevatorFSM* fsm, ElevatorEvent event) = 0;
    virtual ~ElevatorState() {}
};

// 具体状态类
class StoppedState : public ElevatorState {
public:
    void handleEvent(ElevatorFSM* fsm, ElevatorEvent event) override {
        if(event == BUTTON_PRESSED) {
            // 执行停止状态下的按钮按下处理
            fsm->setState(new MovingUpState());
        }
    }
};

class MovingUpState : public ElevatorState {
public:
    void handleEvent(ElevatorFSM* fsm, ElevatorEvent event) override {
        if(event == FLOOR_REACHED) {
            // 执行到达楼层的处理
            fsm->setState(new StoppedState());
        }
    }
};

// 上下文类
class ElevatorFSM {
    ElevatorState* currentState;
    
public:
    ElevatorFSM() : currentState(new StoppedState()) {}
    
    void handleEvent(ElevatorEvent event) {
        currentState->handleEvent(this, event);
    }
    
    void setState(ElevatorState* newState) {
        delete currentState;
        currentState = newState;
    }
    
    ~ElevatorFSM() { delete currentState; }
};

状态模式的优点是每个状态的逻辑被封装在独立的类中,符合单一职责原则。缺点是会产生大量小类,如果状态很多会导致类爆炸。在实际项目中,我通常会结合状态模式和查表法,用查表法处理简单的状态转换,用状态模式处理复杂的状态逻辑。

3. Autoware状态机代码解析

3.1 状态定义与枚举

Autoware的状态机定义非常规范,首先来看它的状态枚举:

enum class StateList : int32_t {
    MOVE_FORWARD,        // 向前行驶
    TRAFFIC_LIGHT_STOP,  // 交通灯停车
    LANE_CHANGE,         // 变道
    STOP_SIGN_STOP,      // 停车标志停车
    OBSTACLE_AVOIDANCE,  // 避障
    MISSION_COMPLETE = 100, // 任务完成
    EMERGENCY = -1       // 紧急状态
};

这种枚举定义有几个值得学习的点:

  1. 使用了强类型枚举(enum class)避免命名冲突
  2. 显式指定了底层类型(int32_t)确保跨平台一致性
  3. 为特殊状态(MISSION_COMPLETE和EMERGENCY)赋予了特定的值,便于识别

3.2 状态基类设计

Autoware的状态基类设计体现了良好的抽象:

class BaseState {
public:
    virtual ~BaseState() = default;
    virtual void update(StateContext *context) = 0;
    virtual int32_t getStateName() { return 0; };
    virtual std::unique_ptr<std::string> getStateNameString() { return 0; };
};

这里有几个关键设计决策:

  1. 使用纯虚函数强制子类实现核心逻辑
  2. 提供默认实现减少重复代码
  3. 使用智能指针(std::unique_ptr)管理资源
  4. 同时提供数字和字符串两种状态标识方式

3.3 具体状态实现

以"向前行驶"状态为例:

class StateMoveForward : public BaseState {
public:
    void update(StateContext *context) override {
        if (context->getLightColor() == TrafficLight::RED)
            context->setState(StateTrafficLightStop::create());
            
        if(context->getChangeFlag() == ChangeFlag::right || 
           context->getChangeFlag() == ChangeFlag::left)
            context->setState(StateLaneChange::create());
    }
    
    int32_t getStateName() override { 
        return enumToInteger(StateList::MOVE_FORWARD); 
    }
    
    std::unique_ptr<std::string> getStateNameString() override {
        return std::make_unique<std::string>("MOVE_FORWARD");
    }
    
    static std::unique_ptr<BaseState> create() {
        return std::unique_ptr<BaseState>(new StateMoveForward);
    }
};

这个实现有几个亮点:

  1. 状态转换逻辑集中且清晰
  2. 使用了工厂方法模式(create())创建状态实例
  3. 将枚举转换为字符串和数字的方法很规范
  4. 私有构造函数强制使用工厂方法

3.4 上下文类设计

上下文类StateContext是状态机的核心协调者:

class StateContext {
public:
    StateContext() : state_(StateMoveForward::create()),
                    light_color_(TrafficLight::UNKNOWN),
                    change_flag_(ChangeFlag::unknown) {};
                    
    void setState(std::unique_ptr<BaseState> newState) {
        state_ = std::move(newState);
    }
    
    void update() { state_->update(this); }
    
    // 其他方法...
    
private:
    std::unique_ptr<BaseState> state_;
    TrafficLight light_color_;
    ChangeFlag change_flag_;
};

这个设计有几个值得借鉴的地方:

  1. 使用unique_ptr自动管理状态对象生命周期
  2. 初始状态在构造函数中明确设置
  3. 外部环境信息(交通灯、变道标志)作为成员变量
  4. 简洁的接口设计

4. 自动驾驶状态机实践建议

4.1 状态粒度设计

在设计自动驾驶状态机时,状态的粒度非常重要。太粗会导致逻辑复杂,太细会增加维护成本。根据我的经验,可以遵循以下原则:

  1. 每个状态应该对应一个明确的驾驶行为
  2. 状态间的转换条件应该清晰明确
  3. 特殊状态(如紧急状态)应该有最高优先级
  4. 状态应该与感知、决策、控制模块的接口对齐

4.2 异常处理机制

自动驾驶系统必须有完善的异常处理机制。Autoware的做法很值得参考:

  1. 定义了专门的EMERGENCY状态
  2. 紧急状态使用特殊枚举值(-1)便于快速识别
  3. 任何状态都可以直接转换到紧急状态
  4. 紧急状态有独立的处理逻辑
void StateEmergency::update(StateContext *context) {
    // 立即执行紧急停车
    emergencyStop();
    
    // 等待人工干预
    if(humanInterventionReceived()) {
        context->setState(StateMoveForward::create());
    }
}

4.3 测试策略

测试状态机时,我通常会采用分层策略:

  1. 单元测试:针对每个状态类的测试

    • 验证状态转换逻辑
    • 测试边界条件
  2. 集成测试:测试状态机整体行为

    • 模拟各种事件序列
    • 验证状态转换的正确性
  3. 场景测试:模拟真实驾驶场景

    • 交通灯场景
    • 变道场景
    • 紧急避障场景

4.4 性能考量

在实时系统中,状态机的性能也很关键。一些优化技巧:

  1. 避免在状态转换时进行内存分配(使用对象池)
  2. 高频调用的方法要确保高效
  3. 状态检测逻辑要优化
  4. 考虑使用无锁设计避免线程阻塞
// 使用对象池优化状态创建
class StatePool {
public:
    template<typename T>
    std::unique_ptr<BaseState> getState() {
        if(auto it = pool.find(typeid(T)); it != pool.end() && !it->second.empty()) {
            auto ptr = std::move(it->second.back());
            it->second.pop_back();
            return ptr;
        }
        return T::create();
    }
    
    void returnState(std::unique_ptr<BaseState> state) {
        pool[typeid(*state)].push_back(std::move(state));
    }
    
private:
    std::unordered_map<std::type_index, std::vector<std::unique_ptr<BaseState>>> pool;
};

在实际项目中应用有限状态机时,最重要的是保持设计的简洁性和可维护性。不要过度设计,但也要为未来的扩展留有余地。Autoware的状态机实现提供了一个很好的参考模板,但具体实现还需要根据项目需求进行调整。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐