用C语言给51单片机写个‘双层’状态机:从扫地机器人代码看HSM架构实战

嵌入式开发中,状态机是处理复杂逻辑的利器。但传统平面状态机在面对多层级业务逻辑时,往往显得力不从心。本文将从一个扫地机器人的实际案例出发,带你用C语言实现层次状态机(HSM),在51单片机上构建清晰可控的业务逻辑框架。

1. 为什么扫地机器人需要层次状态机

想象一下你的扫地机器人正在工作时遇到低电量。传统状态机可能这样处理:

enum State {
    IDLE,
    CLEANING,
    CHARGING,
    AVOIDING_OBSTACLE
};

void handle_event(enum State *current_state, enum Event event) {
    switch(*current_state) {
        case CLEANING:
            if(event == LOW_BATTERY) {
                *current_state = CHARGING;
            }
            // 其他事件处理...
    }
}

这种写法的问题很快会暴露:当"低电量"事件发生时,需要手动处理停止清扫、寻找充电座、对接充电等一系列子状态。代码迅速膨胀,状态转换逻辑变得难以维护。

HSM通过状态嵌套解决了这个问题。在HSM中,充电状态可以包含"寻找充电座"、"对接"、"充电中"等子状态,形成清晰的层次关系:

Cleaning (父状态)
├─ NormalCleaning
├─ SpotCleaning
└─ EdgeCleaning

Charging (父状态)
├─ LocatingDock
├─ Docking
└─ Charging

2. HSM核心概念与C语言实现

2.1 状态节点数据结构设计

我们使用结构体表示每个状态节点:

typedef struct StateNode {
    void (*entry_action)(void);
    void (*exit_action)(void);
    struct StateNode *parent;
    struct StateNode *children[MAX_CHILDREN];
    int num_children;
} StateNode;

关键设计要点:

  • entry/exit动作:进入/退出状态时执行的操作
  • 父状态指针:实现状态层级的关键
  • 子状态数组:支持状态嵌套

2.2 状态转换的黄金法则

HSM的状态转换遵循三个原则:

  1. 退出当前状态链:从当前状态开始,依次向上执行exit动作,直到遇到共同祖先
  2. 进入目标状态链:从共同祖先开始,向下执行entry动作,直到目标状态
  3. 最小化转换路径:只执行必要的entry/exit动作

用代码表示这一过程:

void transition_to(StateNode *target) {
    // 1. 找出共同祖先
    StateNode *common_ancestor = find_common_ancestor(current_state, target);
    
    // 2. 退出当前状态链
    StateNode *s = current_state;
    while(s != common_ancestor) {
        if(s->exit_action) s->exit_action();
        s = s->parent;
    }
    
    // 3. 进入目标状态链
    StateNode *path[MAX_DEPTH];
    int path_len = build_path(common_ancestor, target, path);
    
    for(int i=0; i<path_len; i++) {
        if(path[i]->entry_action) path[i]->entry_action();
    }
    
    current_state = target;
}

3. 扫地机器人HSM实战实现

3.1 状态树构建

我们为扫地机器人设计如下状态层次:

// 顶层状态
StateNode root = {
    .entry_action = NULL,
    .exit_action = NULL,
    .parent = NULL
};

// 一级状态
StateNode idle = { .parent = &root };
StateNode cleaning = { .parent = &root };
StateNode charging = { .parent = &root };
StateNode error = { .parent = &root };

// 二级状态(清洁子状态)
StateNode normal_cleaning = { .parent = &cleaning };
StateNode spot_cleaning = { .parent = &cleaning };
StateNode edge_cleaning = { .parent = &cleaning };

// 二级状态(充电子状态)
StateNode locating_dock = { .parent = &charging };
StateNode docking = { .parent = &charging };
StateNode charging_in_progress = { .parent = &charging };

3.2 事件处理机制

事件处理需要考虑状态层级:

void handle_event(StateNode *current, Event event) {
    StateNode *s = current;
    
    // 自底向上查找能处理该事件的状态
    while(s != NULL) {
        if(state_handles_event(s, event)) {
            // 找到处理者,执行处理
            s->event_handlers[event](event);
            return;
        }
        s = s->parent;
    }
    
    // 没有状态处理该事件
    log_unhandled_event(event);
}

这种设计实现了事件处理的继承性:子状态可以处理特定事件,未处理的事件会自动交由父状态处理。

4. 优化技巧与资源管理

4.1 状态机内存优化

在51单片机等资源受限环境中,可以采用这些优化策略:

  1. 状态标志压缩:使用位域存储状态标志

    typedef struct {
        unsigned is_cleaning : 1;
        unsigned is_charging : 1;
        unsigned cleaning_mode : 2; // 0=normal,1=spot,2=edge
    } StateFlags;
    
  2. 动态分配取舍:预先分配所有状态节点,避免动态内存分配

  3. 表格驱动优化:用查表法替代switch-case

    const EventHandler event_handlers[MAX_STATES][MAX_EVENTS] = {
        [IDLE] = {
            [START_BUTTON] = handle_start,
            // ...
        },
        // ...
    };
    

4.2 调试与日志技巧

添加这些调试支持会事半功倍:

// 状态转换日志
void log_transition(StateNode *from, StateNode *to) {
    printf("State change: ");
    print_state_path(from);
    printf(" -> ");
    print_state_path(to);
    printf("\n");
}

// 打印状态路径
void print_state_path(StateNode *state) {
    if(state->parent) {
        print_state_path(state->parent);
        printf("/");
    }
    printf("%s", state_names[state->id]);
}

5. 真实案例:避障状态处理

当扫地机器人遇到障碍物时,HSM展现出强大优势:

Cleaning
├─ NormalCleaning
│  └─ (遇到障碍物) → AvoidingObstacle
└─ AvoidingObstacle
   ├─ TurnLeft
   ├─ TurnRight
   └─ BackAndTurn

实现代码清晰反映业务逻辑:

void handle_obstacle(Event event) {
    if(current_state == &normal_cleaning) {
        transition_to(&avoiding_obstacle);
        
        // 根据传感器数据选择避障策略
        if(left_sensor > right_sensor) {
            transition_to(&turn_right);
        } else {
            transition_to(&turn_left);
        }
    }
}

这种结构下,当避障完成后,只需简单返回到父状态:

void handle_avoidance_complete(Event event) {
    // 从当前避障子状态返回到AvoidingObstacle
    transition_to(current_state->parent);
    
    // 然后返回到Cleaning状态
    transition_to(current_state->parent);
}

6. 状态机与调度器集成

在实际系统中,状态机需要与主调度器配合工作:

void main_loop(void) {
    init_hsm();
    
    while(1) {
        // 1. 读取传感器数据
        poll_sensors();
        
        // 2. 处理定时事件
        check_timers();
        
        // 3. 处理外部事件
        Event event = get_next_event();
        if(event != NO_EVENT) {
            handle_event(current_state, event);
        }
        
        // 4. 执行当前状态的持续行为
        if(current_state->do_action) {
            current_state->do_action();
        }
        
        // 5. 低功耗处理
        enter_idle_if_possible();
    }
}

这种架构下,状态机成为系统的核心决策引擎,而调度器负责协调各个模块的执行。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐