引言
在 Arduino 这类资源受限的嵌入式平台上,实时多任务需求常因内存和 CPU 限制而无法使用 RTOS。协程(Coroutine)提供了一种轻量级的协作式多任务方案,通过用户态任务切换,实现并发逻辑,而无需操作系统内核。本文将深入探讨 Arduino 上协程库的实现原理,重点分析其内存开销,并给出实用代码示例。
协程库原理
1. 协程 vs 线程
- 线程:由操作系统调度,每个线程拥有独立栈,切换由内核完成,开销大(上下文切换需保存 CPU 寄存器、栈指针等)。
- 协程:用户态调度,任务间协作式切换,通常共享单一栈或使用静态状态机,切换开销极小(仅保存少量变量)。
在 Arduino 上,协程库常见两种实现:
基于 setjmp/longjmp 的协程
- 利用 C 标准库的
setjmp和longjmp保存/恢复执行环境(包括程序计数器、栈指针等)。 - 每个协程拥有独立的栈(或使用动态分配),切换时保存当前上下文到
jmp_buf,然后跳转到目标协程的jmp_buf。 - 优点:支持局部变量和嵌套调用;缺点:需要为每个协程分配栈,内存开销较大。
基于静态状态机的协程(如 Protothreads)
- 使用宏和
switch-case实现,将函数分割为多个状态,通过静态变量保存状态值。 - 切换时仅需改变状态变量,不涉及栈切换,内存开销极小(仅需几个字节)。
- 缺点:不支持局部变量(需用静态变量替代),且函数内不能有阻塞调用(如
delay)。
2. 内存开销分析
| 实现方式 | RAM 开销(每任务) | Flash 开销(代码) | 切换开销 | 适用场景 | |----------|-------------------|-------------------|----------|----------| | setjmp/longjmp | 栈大小(通常 256B~1KB)+ jmp_buf(约 16B) | 中等(库代码) | 微秒级 | 需要复杂任务逻辑 | | 静态状态机 | 状态变量(1~4B)+ 静态局部变量 | 较小(宏展开) | 纳秒级 | 简单周期性任务 |
- setjmp/longjmp 协程:每个任务需独立栈,Arduino Uno(2KB RAM)最多支持 2~3 个任务(每个 512B 栈)。
- 静态状态机协程:几乎不占额外 RAM,适合大量轻量任务(如 10+ 个)。
配置步骤
以 Arduino IDE 为例,使用 Tasker 库(基于 setjmp)和 Protothreads 库(静态状态机)演示。
1. 安装库
- 打开 Arduino IDE,进入 工具 > 管理库,搜索并安装
Tasker和Protothreads。
2. 基本配置
- 对于 Tasker:包含头文件
Tasker.h,创建任务函数,并在setup()中初始化。 - 对于 Protothreads:包含
pt.h,定义PT_THREAD宏。
完整代码示例
示例 1:使用 Tasker(基于 setjmp)
#include <Tasker.h>
Tasker tasker;
void task1() {
while (1) {
digitalWrite(13, HIGH);
tasker.delay(500); // 非阻塞延时
digitalWrite(13, LOW);
tasker.delay(500);
}
}
void task2() {
while (1) {
digitalWrite(12, HIGH);
tasker.delay(1000);
digitalWrite(12, LOW);
tasker.delay(1000);
}
}
void setup() {
pinMode(13, OUTPUT);
pinMode(12, OUTPUT);
tasker.addTask(task1, 512); // 分配 512B 栈
tasker.addTask(task2, 512);
tasker.start();
}
void loop() {
// 空,Tasker 接管调度
}
示例 2:使用 Protothreads(静态状态机)
#include <pt.h>
static struct pt pt1, pt2;
static int task1(struct pt *pt) {
PT_BEGIN(pt);
while (1) {
digitalWrite(13, HIGH);
PT_YIELD(pt); // 让出 CPU
digitalWrite(13, LOW);
PT_YIELD(pt);
}
PT_END(pt);
}
static int task2(struct pt *pt) {
PT_BEGIN(pt);
while (1) {
digitalWrite(12, HIGH);
PT_YIELD(pt);
digitalWrite(12, LOW);
PT_YIELD(pt);
}
PT_END(pt);
}
void setup() {
pinMode(13, OUTPUT);
pinMode(12, OUTPUT);
PT_INIT(&pt1);
PT_INIT(&pt2);
}
void loop() {
task1(&pt1);
task2(&pt2);
}
注意事项
-
阻塞调用:协程中严禁使用
delay()或阻塞 I/O,否则会阻塞所有任务。应使用非阻塞延时(如millis()或库提供的延时)。 -
局部变量:在 Protothreads 中,局部变量在
PT_YIELD后不会保留,必须使用static变量。 - 栈大小:使用 setjmp 协程时,栈大小需根据任务复杂度调整,过小会导致溢出,过大浪费 RAM。
- 调度公平性:协作式调度依赖任务主动让出,若某任务长时间运行,会饿死其他任务。
- 中断安全:协程切换发生在主循环中,若在中断中调用,需额外保护(如关中断)。
总结
Arduino 上的协程库提供了一种轻量级多任务方案,基于 setjmp 的实现灵活但内存开销大,静态状态机实现极省内存但限制多。开发者应根据任务数量和复杂度选择合适方案。理解其原理和内存开销,能帮助在资源受限的嵌入式系统中高效设计并发逻辑。