单片机资源紧张时不用RTOS而选协作式调度器,因其无需独立栈、无抢占开销、避免中断嵌套与内存碎片;它依赖任务主动yield(),要求函数可重入且不阻塞,通过静态数组+游标管理任务,主循环用时间戳差值触发到期任务,确保低RAM下稳定轮询。

为什么单片机上不用RTOS而选协作式调度器
资源紧张的MCU(比如STM32F0、ESP32-C3在深度省电模式下、或8位AVR)跑不了FreeRTOS这类抢占式RTOS——堆栈开销大、中断嵌套深、内存碎片风险高。协作式调度器不依赖硬件定时器中断做任务切换,所有任务主动让出CPU(yield()),避免了上下文保存/恢复开销和临界区嵌套问题,适合裸机环境下的LED闪烁、传感器轮询、串口收发等周期性但非硬实时的场景。
关键前提:每个任务函数必须是**可重入且不阻塞**的,不能出现while(1)死等、不能调用delay_ms()这种忙等函数,否则整个调度器卡死。
核心结构体怎么定义才节省RAM又支持动态增删
不能用链表(指针多占2–4字节/节点,且分配失败难处理),推荐静态数组+游标管理。每个任务项只存最必要的字段:
-
func_ptr:函数指针类型为void (*)(),不带参数、无返回值 -
interval_ms:下次执行间隔,单位毫秒(注意:不是周期!是“距上次执行后多久再跑”) -
last_run_ms:上次执行的系统滴答时间戳(uint32_t,用HAL_GetTick()或自研systick计数器) -
enabled:bool,运行时开关,比删数组元素便宜得多
示例定义:
typedef struct {<br> void (*func_ptr)();<br> uint32_t interval_ms;<br> uint32_t last_run_ms;<br> bool enabled;<br>} task_t;<br><br>static task_t g_tasks[MAX_TASKS] = {0}; // 全局静态,零初始化安全
立即学习“C++免费学习笔记(深入)”;
时间片轮询主循环怎么写才不漏任务也不超时
主循环不是“每1ms扫一遍所有任务”,而是用当前时间戳减去last_run_ms,只触发那些“已到期且启用”的任务。重点在**避免累加误差**和**防止连续执行**:
- 每次检查用
if (now - task.last_run_ms >= task.interval_ms),而不是if (now % task.interval_ms == 0)(后者在跳过多个周期时会丢失一次) - 执行完任务后必须更新
last_run_ms = now,不能写成last_run_ms += interval_ms(时钟跳变或任务执行耗时长会导致漂移) - 主循环本身要尽量轻量,别在里头做浮点运算或字符串拼接;如果某个任务执行太久(>1ms),后续任务可能集体延迟,需靠设计约束(如UART发送用DMA+回调,而非轮询等待TXE)
典型主循环片段:
uint32_t now = HAL_GetTick();<br>for (uint8_t i = 0; i < MAX_TASKS; i++) {<br> if (!g_tasks[i].enabled || !g_tasks[i].func_ptr) continue;<br> if (now - g_tasks[i].last_run_ms >= g_tasks[i].interval_ms) {<br> g_tasks[i].func_ptr();<br> g_tasks[i].last_run_ms = now; // 关键:重置为当前时刻<br> }<br>}
如何让任务“协作”而不互相干扰
协作的本质是任务自己决定何时交还控制权。没有yield()系统调用,只有约定俗成的规则:
- 每个任务函数末尾不加
return也行,但**绝不能用while(!flag)卡住**——应改用状态机,一次调用只推进一个状态(例如:UART接收任务,一次只查一次RXNE标志,有数据就收一字节并更新缓冲区索引,没数据就直接返回) - 共享资源(如全局缓冲区、外设寄存器)不加锁,但靠“任务间无重叠执行”天然互斥;若某任务需长时间独占资源(如SPI Flash写入),应拆成多个小步,每步后返回,靠调度器下次再续
- 需要延时的逻辑(如“按键消抖后500ms执行动作”)不能用
delay_ms(500),而是在任务里维护私有状态变量 + 时间戳,下次被调度时再判断是否超时
容易被忽略的一点:系统滴答源(HAL_GetTick())必须稳定更新。如果关闭了SysTick或低功耗模式下停掉了systick,now会停滞,所有任务冻结。务必确认你的滴答源在所有工作模式下都有效。


















