【有限状态机实战】- 从理论到Autoware自动驾驶状态机代码解析
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 // 紧急状态
};
这种枚举定义有几个值得学习的点:
- 使用了强类型枚举(enum class)避免命名冲突
- 显式指定了底层类型(int32_t)确保跨平台一致性
- 为特殊状态(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; };
};
这里有几个关键设计决策:
- 使用纯虚函数强制子类实现核心逻辑
- 提供默认实现减少重复代码
- 使用智能指针(std::unique_ptr)管理资源
- 同时提供数字和字符串两种状态标识方式
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);
}
};
这个实现有几个亮点:
- 状态转换逻辑集中且清晰
- 使用了工厂方法模式(create())创建状态实例
- 将枚举转换为字符串和数字的方法很规范
- 私有构造函数强制使用工厂方法
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_;
};
这个设计有几个值得借鉴的地方:
- 使用unique_ptr自动管理状态对象生命周期
- 初始状态在构造函数中明确设置
- 外部环境信息(交通灯、变道标志)作为成员变量
- 简洁的接口设计
4. 自动驾驶状态机实践建议
4.1 状态粒度设计
在设计自动驾驶状态机时,状态的粒度非常重要。太粗会导致逻辑复杂,太细会增加维护成本。根据我的经验,可以遵循以下原则:
- 每个状态应该对应一个明确的驾驶行为
- 状态间的转换条件应该清晰明确
- 特殊状态(如紧急状态)应该有最高优先级
- 状态应该与感知、决策、控制模块的接口对齐
4.2 异常处理机制
自动驾驶系统必须有完善的异常处理机制。Autoware的做法很值得参考:
- 定义了专门的EMERGENCY状态
- 紧急状态使用特殊枚举值(-1)便于快速识别
- 任何状态都可以直接转换到紧急状态
- 紧急状态有独立的处理逻辑
void StateEmergency::update(StateContext *context) {
// 立即执行紧急停车
emergencyStop();
// 等待人工干预
if(humanInterventionReceived()) {
context->setState(StateMoveForward::create());
}
}
4.3 测试策略
测试状态机时,我通常会采用分层策略:
-
单元测试:针对每个状态类的测试
- 验证状态转换逻辑
- 测试边界条件
-
集成测试:测试状态机整体行为
- 模拟各种事件序列
- 验证状态转换的正确性
-
场景测试:模拟真实驾驶场景
- 交通灯场景
- 变道场景
- 紧急避障场景
4.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的状态机实现提供了一个很好的参考模板,但具体实现还需要根据项目需求进行调整。
更多推荐
所有评论(0)