不要让你的 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 里做一切。你只需要把阻塞的东西放到正确的地方。
Comments NOTHING