引言

在 Arduino 这类资源受限的嵌入式平台上,实时多任务需求常因内存和 CPU 限制而无法使用 RTOS。协程(Coroutine)提供了一种轻量级的协作式多任务方案,通过用户态任务切换,实现并发逻辑,而无需操作系统内核。本文将深入探讨 Arduino 上协程库的实现原理,重点分析其内存开销,并给出实用代码示例。

协程库原理

1. 协程 vs 线程

  • 线程:由操作系统调度,每个线程拥有独立栈,切换由内核完成,开销大(上下文切换需保存 CPU 寄存器、栈指针等)。
  • 协程:用户态调度,任务间协作式切换,通常共享单一栈或使用静态状态机,切换开销极小(仅保存少量变量)。

在 Arduino 上,协程库常见两种实现:

基于 setjmp/longjmp 的协程

  • 利用 C 标准库的 setjmplongjmp 保存/恢复执行环境(包括程序计数器、栈指针等)。
  • 每个协程拥有独立的栈(或使用动态分配),切换时保存当前上下文到 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,进入 工具 > 管理库,搜索并安装 TaskerProtothreads

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