目录

引言

一、什么是「阻塞」?为什么要避免它?

二、什么是「非阻塞状态机」?

1. 初始化阶段

2. 启动一次测量

3. 驱动主循环 —— 非阻塞状态机

三、拿到一个全新传感器,你该怎么做?

四、非阻塞状态机的好处

番外:

想象一个没画图的代码

怎么画「发命令→等时间→读数据→校验→上报」的流程图?

流程图

时序图

拿到一个新传感器,怎么做状态机驱动?

步骤一:读手册,搞清流程

步骤二:画流程图/时序图

步骤三:写状态机框架

一个真实例子:非阻塞读取温湿度传感器


引言

  • 嵌入式开发中,常常需要和各种传感器打交道:温湿度、PM2.5、加速度……这些设备往往要经过一系列(或长或短)的时序:发送命令、等待转换、读取数据、CRC 校验、上报结果。许多初学者会在驱动里直接写 DelayMs(500)while(!flag),这很容易把 CPU “卡死”在一个任务上,导致界面卡顿、按键无响应、Wi-Fi 掉线,甚至整个系统崩溃。
  • 非阻塞状态机,正是解决这类问题的利器:它将一次完整的操作拆成若干步,每步都只做一点小事情,再把控制权立刻交给调度器或主循环,让 CPU 能去跑其他任务。下文我们以“拿到一个新传感器驱动”的流程为例,带你一步步理解如何从零开始构建一个非阻塞状态机。

一、什么是「阻塞」?为什么要避免它?

// 典型的“阻塞式”传感器读写
void ReadSensorBlocking(void) {
    I2C_Start();       
    I2C_SendByte(CMD_START_MEAS);
    DelayMs(500);          // 等待传感器转换完成 —— 阻塞500ms
    uint16_t raw = I2C_ReadWord();
    I2C_Stop();
    Process(raw);
}
  • 问题DelayMs(500) 期间,CPU 什么也不干,被死死占用。

  • 后果:若系统还有按键、显示、Wi-Fi、其他传感器……都得排队等你 500 ms,响应太慢会给用户糟糕体验。

二、什么是「非阻塞状态机」?

核心思想:将一次“读传感器”的流程拆成 N 步,每一步只做一点小动作,再马上退出,让主循环或调度器去执行下一步时机。

  1. 状态(State):记录当前在第几步

  2. 事件(Event):外部驱动状态切换的触发条件(定时到、数据就绪、中断通知……)

  3. 动作(Action):状态切换时要执行的操作

// 状态机各状态枚举
typedef enum {
    S_IDLE,         // 空闲:尚未发起任何操作
    S_START_CMD,    // 启动命令:向外设发送启动指令
    S_WAIT_READY,   // 等待就绪:等待外设响应“准备好”信号或超时
    S_READ_DATA,    // 读取数据:从外设读取返回的数据
    S_PROCESS,      // 处理数据:对读取到的数据进行校验或计算
} SmState_t;
 
// 状态机上下文结构体
typedef struct {
    SmState_t state;      // 当前状态
    uint64_t  timestamp;  // 时间戳,用于记录每个状态入口时的系统时间(ms),便于超时检查
    uint16_t  result;     // 存放读取或处理得到的结果值
} SmCtx_t;
 
// 全局状态机实例,初始状态为空闲
SmCtx_t ctx = {
    .state     = S_IDLE,
    .timestamp = 0,       // 初始化时间戳为 0
    .result    = 0,       // 初始化结果为 0
};

三、一步步搭建 —— 以“触发式 I²C 传感器”为例

假设传感器协议:

  • 发送命令 0xA5 启动测量

  • 需 200 ms 转换

  • 然后连续读 2 字节结果,再做 CRC

1. 初始化阶段

void SensorDrvInit(void) {
    // 配置软件 I²C 引脚为开漏
    // 注册一个毫秒级定时器回调,调用 SensorDrvProc()
}

2. 启动一次测量

在某个任务或按键事件里,你调用:

/**
 * @brief 触发一次测量流程的开始
 *
 * 当状态机处于空闲状态时,切换到发送命令状态并记录启动时间戳,
 * 后续测量驱动函数会根据这个状态和时间戳继续往下走。
 */
void StartMeasurement(void) {
    // 仅在空闲状态下才能触发一次新的测量
    if (ctx.state == S_IDLE) {
        ctx.state     = S_START_CMD;         // 进入“发送启动命令”状态
        ctx.timestamp = GetSysRunTime();     // 记录当前系统运行时间,用于后续超时判断
    }
}

3. 驱动主循环 —— 非阻塞状态机

/**
 * @brief 非阻塞式传感器驱动主流程
 * 
 * 通过状态机按步骤完成:发送命令 → 等待转换 → 读取数据 → 校验并回调
 * 每次调用本函数都会根据当前状态机状态做对应动作,不会阻塞。
 */
void SensorDrvProc(void) {
    uint64_t now = GetSysRunTime();  // 当前系统运行时间(ms)

    switch (ctx.state) {
    case S_IDLE:
        // 空闲状态:无需任何操作,等外部调用 StartMeasurement() 触发
        break;

    case S_START_CMD:
        // 1) 发送测量启动命令到传感器
        //    这里使用底层 I2C 写函数,将命令 0xA5 发给指定地址
        BbI2c_StartWrite(SENSOR_ADDR, 0xA5);
        // 切换到“等待传感器就绪”状态
        ctx.state     = S_WAIT_READY;
        // 记录命令发送时刻,用于后续超时判断
        ctx.timestamp = now;
        break;

    case S_WAIT_READY:
        // 2) 等待 200ms,给传感器足够时间进行内部转换
        //    使用时间差判断,非阻塞方式,不会卡住 CPU
        if (now - ctx.timestamp >= 200) {
            ctx.state = S_READ_DATA;
        }
        break;

    case S_READ_DATA:
        // 3) 发送读数据命令并启动接收(可用中断或 DMA)
        BbI2c_StartRead(SENSOR_ADDR, 2);
        // 启动后数据最终会通过 DMA 填充到 buffer
        // 立即切换到 PROCESS 状态,实际接收结果由 bufferReady 标志判断
        ctx.state = S_PROCESS;
        break;

    case S_PROCESS:
        // 4) 检查 bufferReady 标志——表示数据已接收完毕
        if (bufferReady) {
            uint8_t *p   = buffer;                   // 指向接收缓冲区
            uint16_t raw = (p[0] << 8) | p[1];       // 拼接高低字节
            // 校验 CRC(假设第 2 索引存校验码)
            if (CheckCrc(p, 2, p[2])) {
                ctx.result = raw;                    // 保存有效结果
                OnNewData(raw);                     // 回调给上层业务处理
            }
            // 完成一次测量流程,回到空闲状态
            ctx.state = S_IDLE;
        }
        break;
    }
}
  • 没有任何 DelayMs(),只是在 S_WAIT_READY 状态下用时间戳判断

  • 中断或 DMA 完成数据接收,再在状态机里检查 bufferReady

  • 每次 SensorDrvProc() 调用只做一次 switch 分支,动作非常短

三、拿到一个全新传感器,你该怎么做?

  1. 研读数据手册:读懂时序、命令、等待时间、地址和校验方式

  2. 画流程图:把“发命令→等时间→读数据→校验→上报”画成几行时序图

  3. 定义状态 & 上下文:状态机里要哪些状态?需要哪些上下文变量?

  4. 选触发方式

    • 中断驱动(UART、GPIO、DMA 完成中断)+标志位

    • 定时轮询(1 ms 调度)+时间戳

  5. 编写状态机:用 switch(state) 实现,注意每个分支要

    • 做一件事:发命令、检查超时、读取数据、回调

    • 切换状态

    • 记录时间戳(如需要)

  6. 集成到主循环/操作系统:把 SensorDrvProc() 放到主循环或定时器回调里

  7. 添加日志跟踪:每次状态切入打 DBG_log("state=%d\n", state);,方便调试

  8. 测试 & 校验:确认在掉线、数据超时、校验失败时能重试或上报错误

四、非阻塞状态机的好处

优势说明
≈ 零 CPU 冲突没有大块 Delay,其他任务立即响应,比如 UI、通信、按键……
易于并行可以同时跑多个状态机:I²C、串口、Wi-Fi 连接、HMI 刷新……
灵活可配置改协议、改超时值、加重试都只要在状态机里加几行,不改整体框架
易于单元测试每个状态分支都能独立测试;状态转移也能写成表格,自动化验证
高可靠性超时、错误处理都显式在状态图里,不会被 “卡死”

番外:

看到这里想必大家肯定还是有一点迷惑的,为什么画流程图,不画不行么?

一句话:为了让复杂流程可视化,简洁、清晰、低出错率地写代码。

不画图,你脑子里装的都是「模糊印象」;画了图,逻辑清晰、流程明确、容易编码、方便调试。

想象一个没画图的代码:

        你想驱动一个传感器采集数据,一上来就写代码:

// 向传感器发送测量启动命令
send_command();
delay_ms(20);
read_data();
if (check_crc(data)) {
   report(data);
}

你以为很完美,实际上:

  • 📌 这是阻塞写法(delay 会卡住 CPU)

  • 📌 没考虑异常情况(比如数据没回来?校验错了?)

  • 📌 以后加功能(比如读失败要重发)你就得全改

  • 📌 别人(包括半年后的你)看不懂你在干嘛

怎么画「发命令→等时间→读数据→校验→上报」的流程图?

我们来画个状态转换流程图,然后再看成时序图

流程图:

时序图:

拿到一个新传感器,怎么做状态机驱动?

步骤一:读手册,搞清流程

看传感器 datasheet 或例程,一般都会写:

  • 要先发命令

  • 要等多长时间

  • 怎么读取

  • 怎么判断成功(比如校验位、返回码)

步骤二:画流程图/时序图

就像上面那样,把每一步变成状态,搞清楚状态转移条件。

步骤三:写状态机框架

你只写一小段代码来驱动每个状态,不需要一次做完,不阻塞也不怕中断打断。

一个真实例子:非阻塞读取温湿度传感器

以 DHT11 为例(虽然它协议奇葩,但流程是典型的):

  • 发开始信号(拉低 18ms)→ 延迟 → 读取响应 → 读取 40bit 数据 → CRC → 上报

用状态机来写:

// DHT11 传感器非阻塞状态机状态枚举
typedef enum {
  DHT_IDLE,       // 空闲状态:未开始采集
  DHT_START,      // 启动状态:发送启动信号给传感器
  DHT_WAIT_RESP,  // 等待应答:等待传感器拉低线并开始回应
  DHT_READ_DATA,  // 读取数据:按时序收集 40 位原始数据
  DHT_CHECK,      // 校验数据:对 40 位数据做 CRC 或校验和验证
  DHT_REPORT      // 上报数据:将温湿度值推送给业务层
} DHT_State;

// 全局或静态保存当前状态
static DHT_State dht_state = DHT_IDLE;

// 主任务函数,每次定时调用一次,按照状态推进
void DHT_Task(void) {
  switch (dht_state) {
    case DHT_IDLE:
      // 空闲时机:检查是否到了下一次采集时间点
      if (time_to_start()) {
        send_start_signal();   // 拉低数据线至少 18ms,通知 DHT 开始测量
        start_timer();         // 重置并启动定时器,用于测量等待时长
        dht_state = DHT_WAIT_RESP;
      }
      break;

    case DHT_WAIT_RESP:
      // 等待传感器回应信号(约 20–40μs 高低电平信号)
      if (timer_expired()) {
        // 超过预期时间后,切换到读取数据状态
        dht_state = DHT_READ_DATA;
      }
      break;

    case DHT_READ_DATA:
      // 按 DHT 协议时序,循环读取 40 位原始数据
      if (read_dht_data()) {
        // 读取成功,进入校验阶段
        dht_state = DHT_CHECK;
      } else {
        // 读取失败(超时、通信错误等),回到空闲重试
        dht_state = DHT_IDLE;
      }
      break;

    case DHT_CHECK:
      // 对读取的 5 个字节(湿度高、湿度低、温度高、温度低、校验和)做校验
      if (check_crc()) {
        // 校验通过,进入上报阶段
        dht_state = DHT_REPORT;
      } else {
        // 校验失败,丢弃数据,回到空闲
        dht_state = DHT_IDLE;
      }
      break;

    case DHT_REPORT:
      // 将最新的温度和湿度值传给上层业务(显示、通信等)
      report_data();
      // 上报完成,重回空闲,等待下一次采集
      dht_state = DHT_IDLE;
      break;
  }
}

结语
非阻塞状态机的核心,就是把“长流程”拆成“短步骤”,用「事件触发」和「时间戳轮询」来控制步骤切换。掌握这套套路后,驱动任何传感器、任何协议,都能写出高性能、高可靠、易维护的嵌入式代码。下次遇到新设备,只要把时序“画图→拆状态→填表→写 switch”这 4 步走完,就大功告成。

创作不易,一键三连(关注,点赞,收藏)

更多推荐