8、位操作与状态机:用位掩码表示状态,用位运算实现状态切换

状态机这东西,做嵌入式的人几乎天天打交道。按键消抖、通信协议解析、任务调度……说白了,就是一个系统在不同状态之间跳来跳去。传统写法喜欢用枚举或者 switch-case,状态一多,代码就变得又臭又长。

我个人习惯用位掩码来表示状态。为什么?因为一个 uint32_t 变量就能装下 32 个独立的状态位,而且状态切换只需要一条位运算指令——快,省内存,还容易扩展。

8.1 状态机的传统实现 vs 位掩码实现

先看一个典型的按键状态机。传统写法是这样的:

typedef enum {
    KEY_IDLE,
    KEY_DEBOUNCE,
    KEY_PRESSED,
    KEY_RELEASED
} key_state_t;

key_state_t state = KEY_IDLE;

void key_process(void) {
    switch (state) {
        case KEY_IDLE:
            if (key_pin_read() == 0) state = KEY_DEBOUNCE;
            break;
        case KEY_DEBOUNCE:
            // 延时消抖...
            if (key_pin_read() == 0) state = KEY_PRESSED;
            else state = KEY_IDLE;
            break;
        // ...
    }
}

这段代码逻辑没问题,但如果你有 5 个按键、每个按键 4 个状态,那就得维护 20 个枚举值。而且状态切换必须用赋值语句,没法并行操作。

用位掩码改写一下:

#define KEY_IDLE        (0x00)
#define KEY_DEBOUNCE    (0x01 << 0)
#define KEY_PRESSED     (0x01 << 1)
#define KEY_RELEASED    (0x01 << 2)

uint8_t key_state = KEY_IDLE;

void key_process(void) {
    if (key_state & KEY_IDLE) {
        if (key_pin_read() == 0) {
            key_state &= ~KEY_IDLE;
            key_state |= KEY_DEBOUNCE;
        }
    }
    else if (key_state & KEY_DEBOUNCE) {
        // 消抖处理...
        if (key_pin_read() == 0) {
            key_state &= ~KEY_DEBOUNCE;
            key_state |= KEY_PRESSED;
        } else {
            key_state &= ~KEY_DEBOUNCE;
            key_state |= KEY_IDLE;
        }
    }
    // ...
}

你可能会问:这看起来也没省多少代码啊?别急,这只是单个状态机。当多个状态机需要共存时,位掩码的优势就出来了。

8.2 多状态并行:一个变量管所有

我在项目中遇到过这样一个场景:一个设备同时管理 8 个传感器,每个传感器有 3 种状态(空闲、采样、报错)。如果用枚举,得定义 24 个状态值。用位掩码呢?一个 uint32_t 就够了。

// 每个传感器占用 2 位,共 8 个传感器,需要 16 位
#define SENSOR_MASK(id)      (0x03 << ((id) * 2))
#define SENSOR_IDLE(id)      (0x00 << ((id) * 2))
#define SENSOR_SAMPLING(id)  (0x01 << ((id) * 2))
#define SENSOR_ERROR(id)     (0x02 << ((id) * 2))

uint32_t sensor_states = 0;

// 设置传感器 3 为采样状态
void set_sensor_sampling(uint8_t id) {
    sensor_states &= ~SENSOR_MASK(id);   // 先清除该传感器的状态位
    sensor_states |= SENSOR_SAMPLING(id); // 再设置新状态
}

// 检查传感器 5 是否报错
uint8_t is_sensor_error(uint8_t id) {
    return (sensor_states & SENSOR_MASK(id)) == SENSOR_ERROR(id);
}

核心思想:每个状态机占用固定宽度的位域,用掩码隔离。状态切换 = 清除旧位 + 设置新位,两步操作可以用一条原子指令完成(如果硬件支持)。

8.3 状态切换的位运算技巧

状态切换说白了就是三件事:进入状态、退出状态、切换状态。用位运算实现起来非常直接。

操作 位运算表达式 说明
进入状态 S state |= S 将 S 对应的位置 1
退出状态 S state &= ~S 将 S 对应的位清 0
切换状态 S state ^= S 位翻转,常用于调试
从 S1 切换到 S2 state = (state & ~S1) | S2 先清除 S1,再设置 S2

小技巧:如果 S1 和 S2 是互斥的(比如同一个状态机的不同状态),可以用 state = (state & ~MASK) | NEW_STATE 一步完成切换。MASK 是 S1 和 S2 所在的位域掩码。

8.4 状态机中的事件驱动

状态机不能光有状态,还得有事件。事件也可以用位掩码表示。比如:

#define EVT_TIMEOUT     (0x01 << 0)
#define EVT_DATA_READY  (0x01 << 1)
#define EVT_ERROR       (0x01 << 2)

uint8_t events = 0;

// 触发事件
events |= EVT_DATA_READY;

// 状态机处理
if (events & EVT_DATA_READY) {
    // 处理数据就绪事件
    events &= ~EVT_DATA_READY;  // 清除事件
}

这样做的好处是:多个事件可以同时触发,状态机一次处理完。我曾经在一个通信协议栈里用这种方式,把事件队列从环形缓冲区简化成了一个 uint32_t 变量,省掉了动态内存分配。

8.5 状态机流程图

下面这张图展示了一个典型的状态机如何用位掩码表示和切换。注意看状态寄存器的每一位或位域对应一个独立状态。

位掩码状态机结构图 状态寄存器 (uint32_t) 位 31 ... 位 16 | 位 15 ... 位 8 | 位 7 ... 位 4 | 位 3 ... 位 0 传感器组 3 (位 12-15) 空闲/采样/报错 传感器组 2 (位 8-11) 空闲/采样/报错 传感器组 1 (位 4-7) 空闲/采样/报错 按键 (位 0-3) 空闲/消抖/按下/释放 事件寄存器 (uint8_t) 位 7 ... 位 3 | 位 2 (超时) | 位 1 (数据就绪) | 位 0 (错误) 清除旧状态 state &= ~MASK 设置新状态 state |= NEW_STATE 检查事件 if (events & EVT) 循环处理,直到事件寄存器为 0 状态域 事件位 切换步骤

8.6 避坑指南

我曾经在一个产品里用位掩码状态机,结果出了个诡异的 bug:两个状态共用了同一个位。原因是宏定义时移位写错了,#define STATE_A (0x01 << 3)#define STATE_B (0x01 << 3) 撞车了。排查了一整天。

教训:每个状态宏定义必须唯一,建议用 #define STATE_A (1ULL << 0) 这种写法,一目了然。另外,给每个状态机分配独立的位域范围,不要交叉。

另一个坑:位运算在 8 位 MCU 上可能不是原子的。如果你在中断里修改状态寄存器,主循环也在读,记得加临界区保护。我一般用 __disable_irq() 包一下,或者用原子操作指令(如果芯片支持)。

8.7 实战建议

  • 状态数量少(≤8 个):用 uint8_t,每位一个状态。切换用 state = (state & ~OLD) | NEW
  • 状态数量中等(8~32 个):用 uint32_t,按功能分组。比如低 8 位给按键,中间 8 位给传感器。
  • 状态数量多(超过 32 个):用多个 uint32_t 组成状态数组,或者考虑用结构体位域。
  • 事件处理:事件寄存器建议用 uint8_t 或 uint16_t,事件处理完立刻清除对应位,避免重复触发。

嗯,位掩码状态机说白了就是用空间换时间、用位运算换分支判断。在资源受限的嵌入式系统里,这招非常实用。你想想看,一个 switch-case 可能编译出几十条指令,而一条 state |= FLAG 只需要一个时钟周期。省下来的 CPU 时间,干点别的不好吗?


公众号:蓝海资料掘金营,微信deep3321