引言

在嵌入式开发中,Arduino 以其简单易用著称,但面对多任务并发需求时,传统的 loop() 轮询方式往往显得力不从心。引入 RTOS(如 FreeRTOS)虽能解决问题,但在 Flash/RAM 有限的芯片上(如 ATmega328P),其开销不容忽视。本文介绍一种基于 Timer1 的协作式优先级调度器,它通过硬件定时器中断驱动任务切换,实现无操作系统的轻量级多任务管理,特别适合对实时性要求不极端、但需要结构化任务管理的场景。

原理:协作式调度与优先级

协作式调度(Cooperative Scheduling)的核心是:任务主动让出 CPU(通过 yield()delay()),而非被系统强制抢占。我们利用 Timer1 产生周期性中断,在中断服务程序(ISR)中检查当前任务是否超时,若超时则保存上下文并切换到下一个就绪的高优先级任务。

优先级体现在任务就绪队列的排序上:我们维护一个任务数组,按优先级从高到低排列,调度器总是选择最高优先级的就绪任务执行。由于是协作式,高优先级任务不会被低优先级任务打断(除非低优先级任务主动让出),但高优先级任务可以通过设置更短的时间片来获得更频繁的 CPU 时间。

关键组件:任务控制块(TCB)

每个任务由一个 TCB 描述,包含:

  • 任务函数指针
  • 任务状态(就绪/阻塞)
  • 优先级
  • 时间片(以毫秒为单位)
  • 剩余时间计数
  • 上下文(保存的寄存器,这里简化处理,因为协作式切换发生在任务函数内部,不需要完整上下文切换)

注意:在 AVR 上,由于中断会保存大部分寄存器,我们只需在任务切换时保存/恢复栈指针和程序计数器(PC),但为了简化,我们采用“任务函数返回后调度”的方式,即每个任务是一个无限循环,但通过 yield() 主动让出。

硬件配置:Timer1 设置

ATmega328P 的 Timer1 是 16 位定时器,适合产生较长周期中断。我们配置为 CTC 模式(Clear Timer on Compare Match),产生 1ms 的节拍。

寄存器配置步骤

  1. 设置比较值OCR1A = 15999;(16MHz 时钟,256 分频,1ms 中断:16000000/256/1000 = 62.5,取整 62,但实际用 15999 对应 1ms?计算:16000000/256 = 62500 Hz,1ms 需要 62.5 个计数,所以 OCR1A = 62-1 = 61?不对,我们使用 CTC 模式,计数到 OCR1A 后清零,所以周期 = (OCR1A+1)/62500 秒,要 1ms,则 OCR1A+1 = 62.5,取 62,则周期约 0.992ms。为精确,可调整。这里为了示例,我们设定 1ms 节拍,OCR1A = 62-1 = 61。但实际常用 1ms 用 15999 是因为 256 分频下 16MHz/256=62500Hz,1ms 需要 62.5 个计数,所以 OCR1A=61。但很多库用 15999 是因为 1024 分频?我们明确:使用 256 分频,OCR1A = 61。

    TCCR1B = (1 << WGM12) | (1 << CS12); // CTC 模式,256 分频
    OCR1A = 61; // 1ms 中断
    TIMSK1 = (1 << OCIE1A); // 使能比较匹配中断
    
  2. 中断服务程序:在 ISR 中调用调度器 scheduler_tick()

调度器实现

数据结构

#define MAX_TASKS 4

typedef void (*TaskFunc)();

typedef struct {
    TaskFunc func;
    uint8_t priority;      // 数值越大优先级越高
    uint8_t state;         // 0: 阻塞, 1: 就绪
    uint16_t time_slice;   // 时间片 ms
    uint16_t remaining;    // 剩余时间 ms
} TCB;

TCB tasks[MAX_TASKS];
volatile uint8_t current_task = 0; // 当前运行任务索引
volatile uint8_t task_count = 0;

任务注册与调度逻辑

void scheduler_init() {
    // 初始化 Timer1
    noInterrupts();
    TCCR1B = (1 << WGM12) | (1 << CS12);
    OCR1A = 61;
    TIMSK1 = (1 << OCIE1A);
    interrupts();
}

int8_t task_create(TaskFunc func, uint8_t priority, uint16_t time_slice) {
    if (task_count >= MAX_TASKS) return -1;
    tasks[task_count].func = func;
    tasks[task_count].priority = priority;
    tasks[task_count].time_slice = time_slice;
    tasks[task_count].remaining = time_slice;
    tasks[task_count].state = 1; // 就绪
    // 简单按优先级插入排序,保持数组降序
    int8_t i = task_count - 1;
    while (i >= 0 && tasks[i].priority < priority) {
        tasks[i+1] = tasks[i];
        i--;
    }
    tasks[i+1] = tasks[task_count]; // 注意:这里逻辑有误,需修正
    // 正确做法:先复制原任务到临时,然后移动,最后插入
    // 为简化,我们直接使用冒泡排序,但这里省略,见完整代码
    task_count++;
    return 0;
}

void scheduler_tick() {
    // 递减当前任务的剩余时间
    if (tasks[current_task].remaining > 0) {
        tasks[current_task].remaining--;
    }
    // 如果当前任务时间片用完,则切换到下一个就绪任务(最高优先级)
    if (tasks[current_task].remaining == 0) {
        // 寻找最高优先级就绪任务
        uint8_t next = 0;
        for (uint8_t i = 0; i < task_count; i++) {
            if (tasks[i].state == 1) {
                next = i;
                break;
            }
        }
        // 重置当前任务时间片(但仅当它仍就绪)
        if (tasks[current_task].state == 1) {
            tasks[current_task].remaining = tasks[current_task].time_slice;
        }
        current_task = next;
        tasks[current_task].remaining = tasks[current_task].time_slice;
    }
}

void yield() {
    // 主动让出,立即触发调度(通过设置 remaining=0)
    tasks[current_task].remaining = 0;
    // 等待下一个 tick 进行切换,或者直接调用 scheduler_tick()(但需注意中断嵌套)
    // 这里简单调用 scheduler_tick(),但需关中断
    noInterrupts();
    scheduler_tick();
    interrupts();
}

任务函数示例

void task_high() {
    while (1) {
        digitalWrite(13, HIGH);
        delay(100); // 内部会调用 yield
        digitalWrite(13, LOW);
        delay(100);
    }
}

void task_low() {
    while (1) {
        // 低优先级任务
        Serial.println("Low");
        yield();
    }
}

注意:delay() 函数在 Arduino 中会阻塞,但我们可以重写一个基于调度的 delay(),或者让任务使用 yield() 配合 millis() 实现非阻塞延时。这里为了演示,我们使用 yield() 手动让出。

完整代码示例

#include <Arduino.h>

#define MAX_TASKS 4

typedef void (*TaskFunc)();

typedef struct {
    TaskFunc func;
    uint8_t priority;
    uint8_t state; // 1: ready, 0: blocked
    uint16_t time_slice;
    uint16_t remaining;
} TCB;

TCB tasks[MAX_TASKS];
volatile uint8_t task_count = 0;
volatile uint8_t current_task = 0;

// 简单排序插入(按优先级降序)
void task_create(TaskFunc func, uint8_t priority, uint16_t slice) {
    if (task_count >= MAX_TASKS) return;
    // 找到插入位置
    int8_t pos = task_count;
    for (int8_t i = task_count - 1; i >= 0; i--) {
        if (tasks[i].priority < priority) {
            pos = i;
        } else break;
    }
    // 移动元素
    for (int8_t i = task_count; i > pos; i--) {
        tasks[i] = tasks[i-1];
    }
    // 插入新任务
    tasks[pos].func = func;
    tasks[pos].priority = priority;
    tasks[pos].time_slice = slice;
    tasks[pos].remaining = slice;
    tasks[pos].state = 1;
    task_count++;
}

void scheduler_tick() {
    if (task_count == 0) return;
    // 递减当前任务剩余时间
    if (tasks[current_task].remaining > 0) {
        tasks[current_task].remaining--;
    }
    // 如果时间片用完,切换
    if (tasks[current_task].remaining == 0) {
        // 寻找最高优先级就绪任务(数组已按优先级降序)
        uint8_t next = current_task;
        for (uint8_t i = 0; i < task_count; i++) {
            if (tasks[i].state == 1) {
                next = i;
                break;
            }
        }
        // 重置当前任务时间片(如果它仍就绪)
        if (tasks[current_task].state == 1) {
            tasks[current_task].remaining = tasks[current_task].time_slice;
        }
        current_task = next;
        tasks[current_task].remaining = tasks[current_task].time_slice;
    }
}

void yield() {
    noInterrupts();
    tasks[current_task].remaining = 0;
    scheduler_tick();
    interrupts();
}

// 任务函数
void task_led() {
    pinMode(13, OUTPUT);
    while (1) {
        digitalWrite(13, HIGH);
        delay(200); // 注意:delay会阻塞,但这里我们简单使用,实际应替换为基于调度的延时
        digitalWrite(13, LOW);
        delay(200);
    }
}

void task_print() {
    Serial.begin(9600);
    while (1) {
        Serial.println("Low priority task");
        yield(); // 主动让出
    }
}

void setup() {
    // 初始化调度器
    cli();
    TCCR1B = (1 << WGM12) | (1 << CS12); // CTC, 256分频
    OCR1A = 61; // 1ms
    TIMSK1 = (1 << OCIE1A);
    sei();

    // 创建任务:高优先级 LED 闪烁,低优先级串口打印
    task_create(task_led, 2, 10); // 10ms 时间片
    task_create(task_print, 1, 20); // 20ms 时间片

    // 启动第一个任务
    current_task = 0;
    tasks[current_task].remaining = tasks[current_task].time_slice;
}

void loop() {
    // 调度器由中断驱动,loop 中执行当前任务
    tasks[current_task].func();
}

ISR(TIMER1_COMPA_vect) {
    scheduler_tick();
}

注意事项

  • 中断安全:在 yield() 和调度器内部操作共享变量时,必须关中断(noInterrupts()/interrupts()),防止数据竞争。
  • 任务函数不得阻塞:协作式调度要求任务不能长时间占用 CPU,否则低优先级任务会饿死。应使用非阻塞延时(如基于 millis() 的状态机)或频繁调用 yield()
  • 栈空间:每个任务共享同一个栈(因为协作式切换不切换栈),所以任务函数内局部变量不宜过大,避免溢出。
  • 优先级反转:由于是协作式,高优先级任务必须主动让出,否则低优先级任务永远无法执行。设计时需确保高优先级任务有 yield() 或延时。
  • 时间片精度:OCR1A 的计算需根据实际晶振频率调整,示例中 61 对应 1ms 是基于 16MHz 和 256 分频,若使用其他芯片需重新计算。
  • 任务切换开销:每次切换仅需几个周期,但频繁切换会增加开销,建议时间片不小于 5ms。

总结

本文展示了如何在 Arduino 上利用 Timer1 实现一个轻量级协作式优先级调度器。通过硬件定时器中断驱动任务切换,我们避免了 RTOS 的复杂性和资源占用,同时获得了多任务管理能力。此方法适用于中小型嵌入式项目,尤其适合 I/O 密集型任务。掌握这一技巧,能帮助你更高效地组织代码,提升系统实时性。