在资源受限的单片机上进行产品开发时,我们常常会遇到“多任务”处理的需求,比如需要同时控制 LED 闪烁、扫描按键、采集传感器数据等。
如果使用传统的 while(1) 超级大循环,单个任务的阻塞很容易导致整个系统卡顿;而引入完整的实时操作系统 (RTOS) 又可能占用过多的芯片资源,显得过于“笨重”。
此时,一个名为 Simple Task Scheduler (STS) 的极简任务调度器恰好能填补这一空白。它仅用几十行 C 语言代码便实现了基础的任务调度功能,硬件资源占用极少,堪称小资源单片机裸机开发的优选方案之一。
资源占用与特点
STS 调度器在资源占用上表现出显著优势:
- RAM 占用:仅需存储任务结构体数组。例如配置 8 个任务时,RAM 占用大约为 80 字节。
- FLASH 占用:调度器核心逻辑编译后仅占用 500~1000 字节,无需额外的函数库依赖。
- CPU 占用:采用主循环轮询调度,没有额外的调度开销,CPU 占用率完全由任务自身的执行时间决定。
适用与不适用场景
适用场景:
- 小资源单片机项目:如 51、STM8 等 RAM/FLASH 资源紧张的单片机,用于实现简单的多任务控制(LED、按键、串口通信等)。
- 低复杂度裸机项目:无需信号量、队列等同步机制,仅需按固定周期执行任务的场景(如小家电控制、传感器数据采集)。
- 快速原型验证:快速实现多任务逻辑,无需花费时间移植 RTOS,有助于理解任务调度的核心思想。
不适用场景:
- 需要抢占式调度,对实时性有严格要求的项目。
- 需要多任务同步(如信号量、互斥锁)的复杂项目。
对于此类场景,建议选择 FreeRTOS 或 RT-Thread 等成熟的 RTOS。
代码实现与解析 (以STM32为例)
以下将代码分为“调度器实现”、“硬件适配”和“任务定义”三部分进行展示,这是一个非常清晰的 C/C++ 工程结构。
1. 调度器头文件 (scheduler.h)
此文件定义了任务结构体和调度器的接口函数。
#ifndef __SCHEDULER_H
#define __SCHEDULER_H
#include “stm32f10x.h”
#include <stdint.h>
#include <stdbool.h>
#define MAX_TASKS 8 // 最大任务数,按需调整
// 任务结构体
typedef struct {
void (*task_func)(void); // 任务函数指针
uint32_t interval_ms; // 执行间隔(ms)
uint32_t last_run_time; // 上次执行时间戳
bool is_enabled; // 任务使能标志
} Task_t;
// 函数声明
void Scheduler_Init(void); // 调度器初始化
bool Scheduler_AddTask(void (*func)(void), uint32_t interval, bool enable); // 添加任务
void Scheduler_Run(void); // 调度器主循环
uint32_t Scheduler_GetSysTimeMs(void); // 获取系统时间(ms)
#endif
2. 调度器源文件 (scheduler.c)
此文件包含了调度器的核心逻辑和 1ms 系统时基的实现。
#include “scheduler.h”
static Task_t task_list[MAX_TASKS] = {0};
static uint8_t task_count = 0;
static uint32_t sys_time_ms = 0;
// SysTick中断服务函数:1ms触发,更新系统时间
void SysTick_Handler(void){
sys_time_ms++;
}
// 初始化1ms时间基准(STM32F103 72MHz主频)
static void SysTime_Init(void){
if (SysTick_Config(SystemCoreClock / 1000)) {
while (1); // 初始化失败,可添加错误处理
}
}
// 获取系统时间
uint32_t Scheduler_GetSysTimeMs(void){
return sys_time_ms;
}
// 调度器初始化
void Scheduler_Init(void){
SysTime_Init();
task_count = 0;
sys_time_ms = 0;
// 清空任务列表
for (uint8_t i = 0; i < MAX_TASKS; i++) {
task_list[i].task_func = NULL;
task_list[i].interval_ms = 0;
task_list[i].last_run_time = 0;
task_list[i].is_enabled = false;
}
}
// 添加任务
bool Scheduler_AddTask(void (*func)(void), uint32_t interval, bool enable){
if (func == NULL || interval == 0 || task_count >= MAX_TASKS) {
return false;
}
task_list[task_count].task_func = func;
task_list[task_count].interval_ms = interval;
task_list[task_count].last_run_time = Scheduler_GetSysTimeMs();
task_list[task_count].is_enabled = enable;
task_count++;
return true;
}
// 调度器主循环
void Scheduler_Run(void){
uint32_t current_time = Scheduler_GetSysTimeMs();
for (uint8_t i = 0; i < task_count; i++) {
if (!task_list[i].is_enabled) continue;
// 检查执行间隔(处理时间溢出)
if ((current_time - task_list[i].last_run_time) >= task_list[i].interval_ms) {
task_list[i].task_func();
task_list[i].last_run_time = current_time;
}
}
}
3. 主函数与任务实现 (main.c)
这里展示了如何初始化硬件、定义具体任务,并在主循环中运行调度器。
#include “stm32f10x.h”
#include “scheduler.h”
// 任务1:LED闪烁(500ms一次)
void Task_LED_Flash(void){
GPIO_WriteBit(GPIOC, GPIO_Pin_13, !GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13));
}
// 任务2:按键扫描(10ms一次)
void Task_Key_Scan(void){
if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == RESET) {
GPIO_SetBits(GPIOB, GPIO_Pin_0); // 按键按下,点亮LED
} else {
GPIO_ResetBits(GPIOB, GPIO_Pin_0); // 按键松开,熄灭LED
}
}
// 硬件初始化
void Hardware_Init(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC, ENABLE);
// LED引脚配置(PC13、PB0)
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
GPIO_Init(GPIOC, &GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_Init(GPIOB, &GPIO_InitStruct);
// 按键引脚配置(PA0上拉输入)
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_Init(GPIOA, &GPIO_InitStruct);
}
int main(void){
Hardware_Init(); // 硬件初始化
Scheduler_Init(); // 调度器初始化
// 添加任务
Scheduler_AddTask(Task_LED_Flash, 500, true);
Scheduler_AddTask(Task_Key_Scan, 10, true);
// 主循环
while (1) {
Scheduler_Run(); // 执行调度器
}
}
跨MCU适配要点
STS 的核心逻辑是通用的,关键在于为不同平台提供准确的 1ms 时间基准 (sys_time_ms)。
- C51 单片机:使用定时器0/1中断来更新
sys_time_ms,注意精确配置定时器初值以实现 1ms 中断。
- STM8:使用其高级定时器实现 1ms 中断,替代STM32的SysTick。
- ESP8266/ESP32:使用芯片提供的定时器API(如
millis())获取毫秒级时间戳,调度器核心逻辑无需修改。
理解不同 计算机基础 架构下的定时器工作原理,能帮助你更好地完成适配。
使用注意事项
- 任务函数必须短小:禁止在任务函数中进行死循环或长时间延时。若要处理耗时逻辑,应将其拆分为状态机。
- 合理设置任务间隔:建议任务间隔 >=1ms,避免高频任务过度占用 CPU,影响其他任务执行。
- 按需调整任务数量:根据实际任务数量修改
MAX_TASKS 宏,以减少不必要的 RAM 占用。
- 保证时基准确:确保
sys_time_ms 的 1ms 时基精准稳定,否则会导致任务执行周期产生累积误差。
总结
Simple Task Scheduler (STS) 秉承极简的设计理念,有效解决了单片机裸机开发中多任务调度的痛点,特别适合硬件资源受限的应用场景。
它无需复杂的移植流程,核心代码易于理解和定制,是小项目快速落地和原型验证的理想工具。开发者只需掌握“时间基准 + 任务轮询”这一核心思想,即可针对不同的单片机平台进行快速适配,在确保系统响应能力的同时,最大限度地降低资源消耗。
本文讨论了在资源受限环境下进行任务调度的轻量级解决方案。如果你想了解更多嵌入式开发技巧或与其他开发者交流,欢迎访问 云栈社区 。
