# 引言 在 Arduino 生态中,`loop()` 循环天然是顺序执行的,但真实项目往往需要同时处理传感器采样、通信协议、按键扫描等任务。引入 RTOS 又显得臃肿,此时协作式调度器成为理想折中:任务主动让出 CPU,代码简单、内存占用极低。但若借助定时器中断实现时间片轮转,则可能触发优先级反转——低优先级任务持有共享资源时被中断,高优先级任务等待该资源,而中优先级任务抢占了 CPU,导致高优先级任务被无限期阻塞。 本文将基于 Arduino Uno(ATmega328P)演示一种规避技巧:**协作式任务 + 定时器中断仅做时间基准,不直接抢占**,通过任务状态机与临界区保护,从根源上消除反转。 # 原理剖析 ## 协作式调度的核心 协作式调度依赖任务主动让出(`yield()`),每个任务运行至完成或显式挂起。其优点是无上下文切换开销、无数据竞争(若任务间不共享变量)。但缺点是无法响应紧急事件——若某任务长时间运行,其他任务会饿死。 ## 定时器中断的陷阱 若用定时器中断强制切换任务(如 `setjmp/longjmp` 或直接修改 PC),则形成抢占式调度。此时,若低优先级任务正在修改共享缓冲区,中断触发切换到高优先级任务,高优先级任务访问同一缓冲区,而中优先级任务恰好就绪,则高优先级任务被中优先级任务间接阻塞,形成反转。 ## 规避策略:中断标志 + 协作切换 我们采用**时间片监控 + 协作切换**: - 定时器中断仅设置一个全局标志 `task_tick`,不执行切换。 - 每个任务在循环中检查 `task_tick`,若超时则主动让出。 - 任务间共享数据通过 `cli()`/`sei()` 或原子操作保护,确保中断不会破坏临界区。 这样,中断只提供时间基准,不改变执行流,因此不存在抢占式反转。但需注意:若任务在临界区内超时,则不能立即让出,需先完成临界区,这可能导致时间片延长,但不会反转。 # 实现步骤 ## 1. 定义任务控制块(TCB) ```c #define MAX_TASKS 4 typedef struct { void (*func)(void); // 任务函数指针 uint16_t period; // 周期(ms) uint16_t elapsed; // 已运行时间 uint8_t active; // 是否启用 } Task_t; Task_t tasks[MAX_TASKS]; volatile uint8_t task_tick = 0; // 中断置1,主循环清零 ``` ## 2. 定时器配置(使用 Timer1,1ms 中断) ```c void setup_timer() { cli(); TCCR1A = 0; TCCR1B = (1 << WGM12) | (1 << CS11); // CTC模式,8分频 OCR1A = 1999; // 16MHz/8/2000 = 1kHz TIMSK1 = (1 << OCIE1A); sei(); } ISR(TIMER1_COMPA_vect) { task_tick = 1; // 仅置标志,不切换 } ``` ## 3. 调度器核心 ```c void scheduler_run() { while (1) { for (uint8_t i = 0; i < MAX_TASKS; i++) { if (!tasks[i].active) continue; tasks[i].elapsed += 10; // 假设每次循环约10ms(实际需校准) if (tasks[i].elapsed >= tasks[i].period) { tasks[i].elapsed = 0; tasks[i].func(); // 执行任务 } // 任务内主动让出,但此处不强制 } } } ``` 但上述代码是轮询式,不是协作式。真正的协作式需要任务内部调用 `yield()`。我们改进: ```c volatile uint8_t yield_request = 0; void yield() { yield_request = 1; while (yield_request) { /* 等待调度器清除 */ } } void scheduler_run() { while (1) { for (uint8_t i = 0; i < MAX_TASKS; i++) { if (!tasks[i].active) continue; yield_request = 0; tasks[i].func(); // 任务内部会调用yield() // 若任务未调用yield,则继续执行下一个(但可能阻塞) } } } ``` ## 4. 任务编写示例 ```c void task_led() { static uint8_t state = 0; digitalWrite(13, state); state = !state; yield(); // 主动让出 } void task_sensor() { int val = analogRead(A0); // 处理数据... yield(); } ``` ## 5. 规避优先级反转的关键:临界区保护 当任务需要访问共享资源(如串口、I2C)时,必须关闭中断或使用原子操作: ```c void safe_uart_send(char c) { cli(); // 进入临界区 Serial.write(c); sei(); } ``` 注意:`yield()` 不能在临界区内调用,否则会死锁。因此,任务应设计为: - 快速获取资源,释放后再让出。 - 若资源被占用,则设置标志并立即让出,等待下次轮询。 # 完整示例代码 ```c #include #define MAX_TASKS 3 typedef struct { void (*func)(void); uint16_t period; uint16_t elapsed; uint8_t active; } Task_t; Task_t tasks[MAX_TASKS]; volatile uint8_t task_tick = 0; volatile uint8_t yield_request = 0; void yield() { yield_request = 1; while (yield_request) { /* 等待 */ } } void task_led() { static uint8_t state = 0; digitalWrite(13, state); state = !state; yield(); } void task_print() { static uint16_t count = 0; cli(); Serial.print("Count: "); Serial.println(count++); sei(); yield(); } void task_delay() { // 模拟耗时操作,但内部让出 for (uint8_t i = 0; i < 10; i++) { delay(1); // 注意:delay会阻塞,但这里仅演示 yield(); } } void setup() { Serial.begin(9600); pinMode(13, OUTPUT); // 初始化任务表 tasks[0] = {task_led, 500, 0, 1}; tasks[1] = {task_print, 1000, 0, 1}; tasks[2] = {task_delay, 2000, 0, 1}; // 配置定时器 cli(); TCCR1A = 0; TCCR1B = (1 << WGM12) | (1 << CS11); OCR1A = 1999; TIMSK1 = (1 << OCIE1A); sei(); } ISR(TIMER1_COMPA_vect) { task_tick = 1; } void loop() { // 调度器主循环 static uint16_t tick_count = 0; if (task_tick) { task_tick = 0; tick_count++; for (uint8_t i = 0; i < MAX_TASKS; i++) { if (!tasks[i].active) continue; tasks[i].elapsed += 1; if (tasks[i].elapsed >= tasks[i].period) { tasks[i].elapsed = 0; yield_request = 0; tasks[i].func(); } } } } ``` # 注意事项 - **定时器精度**:上述代码中 `elapsed` 以1ms为增量,但 `loop()` 中执行任务会占用时间,导致时间片漂移。建议在任务函数开头记录 `millis()`,用真实时间差更新 `elapsed`。 - **临界区长度**:`cli()`/`sei()` 之间的代码应尽量短,否则影响中断响应。 - **yield() 死循环**:若调度器未清除 `yield_request`,任务会卡死。确保在任务调用前清零。 - **任务优先级**:协作式调度本身无优先级,但可通过任务顺序模拟。若需严格优先级,可改为事件驱动。 - **中断服务函数**:ISR 中只做标志位操作,避免调用 `Serial` 等非中断安全函数。 # 总结 本文通过定时器中断仅做时间基准、任务主动让出的协作式设计,从机制上避免了优先级反转。此方案适用于任务数量少、实时性要求不苛刻的场景。若需硬实时,可考虑引入优先级继承协议或使用真正的 RTOS。但掌握这种轻量级调度思想,对理解嵌入式系统核心概念大有裨益。