不要让你的 RTOS 状态机堵在 UART 上

Babel36acl 嵌入式实战 无~ 8 次阅读 预计阅读时间: 9 分钟 发布于 1 天前 最后更新于 2 小时前 1908 字


不要让你的 RTOS 状态机堵在 UART 上

> CommTask 模式:一个专用 I/O 任务如何救回被阻塞的流程

## 经典困境

你的 STM32 固件这样运行:一个状态机任务每 10ms 推进一次流程,需要和打印机通信时就发命令等应答。打印机 USART2 是半双工的,发完要等几十到几百毫秒才回。

然后你的状态机卡住了。它不能推进,因为它在等 UART 应答。

与此同时电机还在转、ADC 还在采——但它们都是独立任务。只有状态机卡了。这意味着:**超时计时器不走了、流程不能推进了、新命令不能响应了**。

整个系统变成一个"部分瘫痪"的状态——你说它死了,它还有心跳;你说它活着,它不干活。

## 根因

状态机任务既负责流程决策,又负责阻塞 I/O。这是职责混合。

"阻塞 I/O" 和 "10ms 周期状态机" 是互斥的约束。两者只能选一个。

## 解决方案:CommTask 模式

加一个专用 I/O 任务,专门处理阻塞操作。把 I/O 和流程决策拆成两个任务:

```
StateMachineTask (高优先级, 10ms 周期):
纯 CPU 计算 → 推进状态机 → 投递 I/O 命令 → 下一个周期再看结果

CommTask (低优先级, 50ms 周期 + 命令触发):
收到命令 → 执行阻塞打印机对话 → 存结果 → 空闲
```

## 三个函数

```c
// 投递命令(StateMachineTask 调,不阻塞)
uint8_t comm_task_enqueue(cmd_type_t type);

// 查询结果(StateMachineTask 调,不阻塞)
result_t comm_task_get_result(cmd_type_t type); // BUSY / OK / FAIL

// 注册执行器(初始化时注册)
void comm_task_register_handler(cmd_type_t type, handler_t handler);
```

## 状态机的视角

```c
void state_on_run(void) {
if (first_entry) {
comm_task_enqueue(CMD_PRINTER_SELF_CHECK);
first_entry = 0;
}

result = comm_task_get_result(CMD_PRINTER_SELF_CHECK);
if (result == BUSY) {
return; // 还没完,下次再查
}

if (result == OK) {
transition_to(NEXT_STATE);
} else {
transition_to(ERROR_STATE);
}
}
```

注意这里的关键:状态机**不等待**。投递命令后立即 return,下一周期再来查结果。10ms 一粒,看起来像立即响应。

## 优先级陷阱

CommTask 的优先级必须**低于** StateMachineTask。为什么?

如果 CommTask 优先级更高,当状态机投递命令后,CommTask 抢到 CPU 开始打印对话。但状态机被抢占了——它不能推进流程、不能检查输入、不能处理新到来的协议帧。

正确的调度:

```
StateMachineTask — 最高,周期推进
MotorTask — 次高,执行器响应
CommTask — 低,慢慢等 UART 就好
```

状态机说"去打印",CommTask 说"好,我慢慢弄,你继续走你的"。
如果 CommTask 说"好,我先弄完你别动",那和直接在主任务里调 UART 没区别。

## 什么时候不该用这个模式

CommTask 模式不是银弹。它适合的场景:

| | 适用 | 不适用 |
|---|---|---|
| I/O 耗时 | 几十到几百毫秒(打印机、EEPROM 页写) | 次毫秒级(SPI 读寄存器) |
| 响应要求 | 10ms 级别的延迟可接受 | 需要在 1ms 内确认 |
| 通信方向 | 任务发起 → 设备响应 | 设备主动推送数据 → 任务被动接收 |

对于第三种情况(设备主动推送),应该用事件驱动模型——UART 接收中断 → 解析 → 事件通知 → 任务响应。这里的 CommTask 模式是反过来的:任务主动发起,设备被动应答。

## 另一条隐含收益

CommTask 模式还附带了一个好处:**你自然得到了阻塞 I/O 的节流和序列化**。

如果打印机同时收到两个命令(比如自检过程中又来了透传命令),CommTask 的自然排队机制会保证它们不会打架。在同步模型里你要手写互斥锁。

## 总结

Embedded 的 RTOS 里,"多久能做完" 比 "做得多快" 更重要。StateMachineTask 每 10ms 推进一次,它的"多久能做一次"必须可预测。把不可预测的 I/O 交给 CommTask,状态机保持确定性的 10ms 脉搏。

你不需要在 ISR 里做一切。你只需要把阻塞的东西放到正确的地方。

此作者没有提供个人介绍。
最后更新于 2026-05-30