1. 协程模拟的需求背景
在嵌入式系统和资源受限环境中,开发者常常面临一个现实困境:需要实现多任务协作,但系统却不支持线程或协程。我在开发一款物联网设备固件时就遇到了这个问题——设备需要同时处理传感器数据、网络通信和用户输入,但芯片只有32KB内存,无法运行RTOS。
传统解决方案是用状态机,但状态机的代码结构非常反人类。一个简单的任务流程会被拆分成几十个离散的状态,调试时要在不同case间跳转。这时候,switch-case配合宏定义提供了一种轻量级的协程模拟方案。
2. 基础实现原理
2.1 状态保持机制
核心思路是利用switch-case的跳转特性配合static变量保存状态:
c复制#define TASK_BEGIN() static int _state = 0; switch(_state) { case 0:
#define YIELD() do { _state = __LINE__; return; case __LINE__:; } while(0)
#define TASK_END() }
这个实现有几个关键点:
__LINE__宏记录当前代码行号作为状态标识- static变量保证函数调用间状态持久化
- do-while(0)包裹YIELD确保语法完整性
2.2 典型使用示例
下面是一个串口数据接收的协程模拟:
c复制void uart_routine() {
TASK_BEGIN();
while(!uart_ready()) {
YIELD();
}
uint8_t len = uart_read_len();
YIELD();
uint8_t buf[64];
for(int i=0; i<len; i++) {
buf[i] = uart_read_byte();
YIELD();
}
process_data(buf, len);
TASK_END();
}
每次调用uart_routine()时,程序会从上一次yield的位置继续执行,实现了类似协程的挂起/恢复效果。
3. 进阶优化技巧
3.1 参数传递方案
基础版本无法传递参数,可以通过结构体扩展:
c复制struct task_ctx {
int state;
void* args;
};
#define TASK_BEGIN(ctx) \
struct task_ctx* _ctx = (ctx); \
switch(_ctx->state) { case 0:
#define YIELD(ctx, val) \
do { _ctx->state = __LINE__; \
_ctx->args = (void*)(val); \
return; \
case __LINE__:; } while(0)
3.2 多任务调度器
配合简单的轮询调度器:
c复制#define MAX_TASKS 4
typedef void (*task_func)(struct task_ctx*);
struct scheduler {
task_func tasks[MAX_TASKS];
struct task_ctx contexts[MAX_TASKS];
};
void schedule_run(struct scheduler* s) {
for(int i=0; i<MAX_TASKS; i++) {
if(s->tasks[i]) {
s->tasks[i](&s->contexts[i]);
}
}
}
3.3 栈空间优化
在内存紧张时,可以用共用体减少内存占用:
c复制union task_args {
int i_val;
float f_val;
void* p_val;
};
4. 实际应用中的坑与对策
4.1 变量作用域问题
自动变量在yield后会丢失,必须使用static或全局变量:
c复制// 错误示例
void bad_task() {
TASK_BEGIN();
int counter = 0; // 每次yield后都会重新初始化
YIELD();
counter++;
TASK_END();
}
// 正确做法
void good_task() {
TASK_BEGIN();
static int counter = 0; // 保持状态
YIELD();
counter++;
TASK_END();
}
4.2 调试技巧
由于行号作为状态标识,添加日志时需要特别注意:
c复制#define YIELD() \
do { \
printf("Yield at %s:%d\n", __FILE__, __LINE__); \
_state = __LINE__; \
return; \
case __LINE__:; \
} while(0)
4.3 编译器兼容性
某些编译器可能对__LINE__的实现不同,可以改用枚举定义固定状态:
c复制enum task_states {
STATE_INIT,
STATE_WAIT_UART,
STATE_READ_LEN,
// ...
};
#define TASK_BEGIN() static enum task_states _state = STATE_INIT; switch(_state) { case STATE_INIT:
5. 性能对比测试
在STM32F103C8T6(72MHz Cortex-M3)上的测试数据:
| 实现方式 | 内存占用 | 上下文切换时间 |
|---|---|---|
| 原生RTOS | 8KB | 1.2μs |
| 本方案 | <100B | 0.3μs |
| 传统状态机 | 50B | N/A |
虽然内存占用略高于纯状态机,但代码可维护性显著提升。上下文切换速度比RTOS更快,因为不需要保存完整的线程上下文。
6. 替代方案对比
6.1 Protothreads
Adam Dunkels提出的轻量级方案,原理类似但实现更复杂:
c复制#include "pt.h"
PT_THREAD(example(struct pt *pt)) {
PT_BEGIN(pt);
while(!condition) {
PT_WAIT_UNTIL(pt, event_occurred());
}
PT_END(pt);
}
优点是有完善的超时处理机制,缺点是依赖外部库。
6.2 Duff's Device
另一种利用switch特性的经典技巧:
c复制void send(uint8_t *to, uint8_t *from, int count) {
int n = (count + 7) / 8;
switch(count % 8) {
case 0: do { *to++ = *from++;
case 7: *to++ = *from++;
case 6: *to++ = *from++;
case 5: *to++ = *from++;
case 4: *to++ = *from++;
case 3: *to++ = *from++;
case 2: *to++ = *from++;
case 1: *to++ = *from++;
} while(--n > 0);
}
}
虽然巧妙,但不适合实现协程逻辑。
7. 在特殊场景下的应用
7.1 中断服务例程
在ISR中实现非阻塞处理:
c复制void USART1_IRQHandler() {
static int state = 0;
switch(state) {
case 0:
if(!USART_GetITStatus(USART1, USART_IT_RXNE))
return;
state = 1;
// 故意不break
case 1:
process_rx_data(USART_ReceiveData(USART1));
state = 0;
}
}
7.2 低功耗设备
配合休眠模式实现事件驱动:
c复制void sensor_task() {
TASK_BEGIN();
enable_sensor();
YIELD();
while(!data_ready()) {
enter_low_power();
YIELD();
}
read_sensor_data();
TASK_END();
}
每次yield后设备可以进入休眠,由外部中断唤醒后继续执行。
8. 语言扩展思考
8.1 C++封装
通过类模板实现类型安全:
cpp复制template<typename T>
class Coroutine {
int state = 0;
T value;
public:
bool next() {
switch(state) {
// ...
}
}
T get() const { return value; }
};
8.2 生成器模式
模拟Python的yield功能:
c复制#define GENERATOR(type) struct { int state; type value; }
#define YIELD_VALUE(gen, val) \
do { (gen).state = __LINE__; \
(gen).value = (val); \
return true; \
case __LINE__:; } while(0)
9. 安全注意事项
9.1 栈溢出防护
虽然不用额外栈空间,但递归调用仍危险:
c复制void dangerous_task() {
TASK_BEGIN();
YIELD();
dangerous_task(); // 栈会持续增长
TASK_END();
}
9.2 重入问题
非静态变量导致的竞态条件:
c复制void reentrant_task(int* ptr) {
TASK_BEGIN();
*ptr = 1; // 可能被其他任务修改
YIELD();
use(*ptr); // 值可能已改变
TASK_END();
}
10. 工程实践建议
- 为每个任务添加超时检测:
c复制#define YIELD_TIMEOUT(ms) \
do { \
uint32_t _timeout = get_tick() + (ms); \
while(!condition) { \
if(get_tick() > _timeout) return -ETIMEDOUT; \
YIELD(); \
} \
} while(0)
-
使用代码生成工具自动维护状态枚举,避免手动维护行号
-
在团队项目中统一宏定义规范,建议放在专门的coroutine.h头文件中
-
配合静态分析工具检查未初始化的自动变量
这套方案我在多个量产项目中实际应用过,最典型的案例是一个蓝牙Mesh节点固件,用200字节RAM实现了5个并发任务。关键是要理解这本质上是一种高级状态机,适合对实时性要求高但资源有限的场景。对于复杂业务逻辑,建议还是使用专业的RTOS。
