# 引言 在 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) ```c #include 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(静态状态机) ```c #include 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 的实现灵活但内存开销大,静态状态机实现极省内存但限制多。开发者应根据任务数量和复杂度选择合适方案。理解其原理和内存开销,能帮助在资源受限的嵌入式系统中高效设计并发逻辑。