基于单片机的简易任务调度器实现周期性执行函数
就像有个。
基于51单片机的简易任务调度器实现周期性执行函数
一、什么是任务调度器
任务调度器(Task Scheduler)是一种系统工具或软件组件,用于自动管理任务的执行顺序、时间和资源分配。它的核心目标是高效协调多个任务,确保它们按时、按优先级或按特定条件运行,同时优化系统资源(如CPU、内存等)。
任务调度器可以简单理解为⼀个“时间管理器”,它按预定的时间安排不同的任务去执⾏。就像⼀个管家,每隔⼀段时间就提醒你去做某件事。举个例⼦,定时器中断处理周期性执行函数相当于,你告诉⼀个⼈,20:00可以吃东西,那么如果他有事错过了20:00,20:01再去看时钟,此时就不满⾜20:00的条件的,那么吃东西这个任务就不会得到执⾏。⽽任务调度器相当于,你告诉他,20:00以后可以吃东西,那么哪怕错过了20:00,20:01再去看时钟,也是满⾜20:00以后这个条件的,那么吃东西这个任务就会得到执⾏,就不会出现跳过执⾏的情况了。
主要功能:
- 任务排队:按优先级或规则排列待执行的任务。
- 触发控制:根据时间(如定时任务)、事件(如系统启动)或依赖条件(如前置任务完成)触发任务。
- 资源分配:分配CPU、内存等资源,避免冲突或过载。
- 容错处理:重试失败任务或通知用户。
常见类型:
- 操作系统级:如Linux的cron、Windows的任务计划程序。
- 分布式调度器:如Apache Airflow、Kubernetes CronJob,用于跨多台机器调度任务。
- 实时调度器:用于嵌入式系统,确保高优先级任务立即执行。
应用场景:
- 定时备份数据、发送报表。
- 处理批量作业(如数据分析)。
- 微服务中的异步任务(如订单处理)。
二、任务调度器相对于定时器中断处理周期性执行函数的优点
1、定时器中断处理周期性执行函数----简单但死板
我们在单片机中需要处理周期性执行函数(如硬件定时器或操作系统提供的定时器)时通常选择使用定时器中断来处理。但使用定时器中断来实现周期性执行函数虽然直接有效,但也存在一些明显的缺点,尤其在复杂系统或高可靠性场景中需谨慎考虑。
举个例子:厨师设置一个闹钟(定时器中断),每5分钟响一次,提醒他去搅汤。
- 中断干扰:如果闹钟响时厨师正在切菜(执行关键代码),他必须立刻停下,手忙脚乱去搅汤,可能切到手(数据竞争)。
- 无法灵活调整:如果汤快烧干了,但闹钟还没响,厨师不会提前处理(实时性差)。
- 效率低:如果汤不需要频繁搅动,闹钟仍会不断打断厨师,让他白跑好几趟(资源浪费)。
- 总结:就像强迫症的闹钟,不管你在做什么,到点就必须停下,容易手忙脚乱。
例如以下代码示例:
#include <reg52.h>//单片机寄存器专用头文件
unsigned char Key_Slow_Down;//按键减速专用变量
unsigned int Seg_Slow_Down;//数码管减速专用变量
/* 键盘处理函数 */
void Key_Proc()
{
if(Key_Slow_Down) return;
Key_Slow_Down = 1;//键盘减速程序
}
/* 数码管处理函数 */
void Seg_Proc()
{
if(Seg_Slow_Down) return;
Seg_Slow_Down = 1;//数码管减速程序
}
/* 定时器0中断初始化函数 */
void Timer0Init(void) //1毫秒@12.000MHz
{
AUXR &= 0x7F; //定时器时钟12T模式
TMOD &= 0xF0; //设置定时器模式
TL0 = 0x18; //设置定时初始值
TH0 = 0xFC; //设置定时初始值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0 = 1; //定时器中断0打开
EA = 1; //总中断打开
}
/* 定时器0中断服务函数 */
void Timer0Server() interrupt 1
{
if(++Key_Slow_Down == 10) Key_Slow_Down = 0;//键盘减速专用
if(++Seg_Slow_Down == 500) Seg_Slow_Down = 0;//数码管减速专用
}
/* Main */
void main()
{
while (1)
{
Key_Proc();
Seg_Proc();
}
}
在以上代码中,我们采⽤如Seg_Slow_Down等减速变量来减速程序,实现简单的调度。在定时器中断⾥让减速变量每1毫秒⾃增,到达⼀定的数值后归零,在任务处理函数⾥判断对应的减速变量是否为真,为真则直接返回,为假则执⾏后⾯相关处理内容,以此来实现分时调⽤的目的。
⽽这样就有⼀个逻辑上的问题,即有且只有减速变量等于0的时候后续处理代码才会被执⾏,⽽减速变量为0只能持续1毫秒,1毫秒后⼜在定时器中断⾥⾃增成1了,所以在while(1)⾥循环的⼏个处理函数的扫描必须⼩于1毫秒,否则将有概率跳过处理函数的本次执⾏。
举个例子:
void Seg_Proc()
{
rd_temperature();//执行一个温度读取函数
//程序运行第1毫秒 此时Led_Slow_Down=192
//程序运行第2毫秒 此时Led_Slow_Down=192
......
//程序运行第8毫秒 此时Led_Slow_Down=199
//程序运行第9毫秒 此时Led_Slow_Down=0
//程序运行第10毫秒 此时Led_Slow_Down=1
}
void main()
{
while(1)
{
Key_Proc();
Seg_Proc(); // 当退出该函数时Led_Slow_Down将为1,而其中归0了一次
Led_Proc(); // Led_Slow_Down=1,该函数直接返回
}
}
/* 定时器1中断服务程序 */
void Timer1_Isr(void) interrupt 3
{
if( + Led_Slow_Down = 200)Led_Slow_Down=0;
}
显⽽易⻅当出现这种情况的时候,Led_Proc()函数跳过了⼀次执⾏,这在有的时候可能是致命的,因为它的刷新时间本次由200毫秒百变成400毫秒。
2、任务调度器处理周期性执行函数----灵活高效
我们采⽤全局变量uwTick来计时,在定时器中断⾥每⼀毫秒⾃增⼀,调度器通过检查每个任务的上次运⾏时间和当前时间,来决定是否执⾏任务,这种设计可以确保任务按照预定的周期执⾏。只要满⾜了运⾏的条件,该任务就可以被执⾏。
同样使用厨师煮汤来举个例子:厨师雇了一个智能管家(任务调度器),管家会:
-
监控汤的状态(事件驱动),如果汤快溢出来了,立刻提醒厨师。
-
优化任务顺序:如果厨师正在接电话,管家会稍后提醒他搅汤,而不是强行打断。
-
动态调整:如果汤已经煮好,管家就不再提醒(节省资源)。
在程序中如何实现:
1.任务结构:每个任务包括⼀个任务函数(告诉管家你要做什么)、执⾏时间间隔(多久执⾏⼀次)和上次执⾏时间(管家记录上次提醒你的时间)。
2.任务数组:把所有的任务存储在⼀个列表中。
3.调度器运⾏:调度器会不断检查当前时间,如果到了某个任务的执⾏时间,就执⾏对应的任务函数。
代码示例:
/* 定义了一个名为 scheduler_task_t 的结构体 */
typedef struct {
void (*task_func)(void);//任务函数
uint32_t rate_ms;//任务函数的执行周期,单位为毫秒
uint32_t last_run;//存储任务上次运⾏的时间戳,单位为毫秒
} task_t;
//全局变量,用于存储任务数量
uint8_t task_num;
/* 静态任务数组,每个任务包含任务函数、执行周期(毫秒)和上次运行时间(毫秒) */
static task_t scheduler_task[] =
{
{Led_Proc, 1, 0}, //定义一个任务,任务函数为 Led_Proc,执行周期为 1 毫秒,初始上次运行时间为 0
{Key_Proc, 10, 0}, //定义一个任务,任务函数为 Key_Proc,执行周期为 10 毫秒,初始上次运行时间为 0
};
/* 调度器初始化函数 */
void scheduler_init(void)
{
//计算任务数组的元素个数,并将结果存储在 task_num 中
task_num = sizeof(scheduler_task) / sizeof(task_t);
}
/* 任务调度器执行函数 */
void scheduler_run(void)
{
//遍历任务数组中的所有任务
for (uint8_t i = 0; i < task_num; i++)
{
//获取当前的系统时间(毫秒)
uint32_t now_time = uwTick;
//检查当前时间是否达到任务的执行时间
if (now_time = tasks[i].rate_ms + tasks[i].last_run)
{
//更新任务的上次运行时间为当前时间
scheduler_task[i].last_run = now_time;
//执行任务函数
scheduler_task[i].task_func();
}
}
}
#include <reg52.h>//单片机寄存器专用头文件
uint32_t uwTick;
/* 定时器0中断初始化函数 */
void Timer0Init(void) //1毫秒@12.000MHz
{
AUXR &= 0x7F; //定时器时钟12T模式
TMOD &= 0xF0; //设置定时器模式
TL0 = 0x18; //设置定时初始值
TH0 = 0xFC; //设置定时初始值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0 = 1; //定时器中断0打开
EA = 1; //总中断打开
}
/* 定时器0中断服务函数 */
void Timer0Server() interrupt 1
{
uwTick++;
}
/* 主函数 */
void main()
{
scheduler_init();
Timer0Init();
while(1)
{
scheduler_run();
}
}
优点:
- 减少无谓中断:厨师可以专注当前任务,不被频繁打扰。
- 智能响应:根据实际情况(如汤的沸腾程度)调整提醒时机,而非机械地按固定时间执行。
- 支持多任务:管家还能帮厨师管理煮面、接电话等任务,让厨房运作更流畅。
总结:就像有个贴心的助手,只在真正需要时才提醒你,避免无效忙碌。
对比总结
| 方式 | 定时器中断(闹钟法) | 任务调度器(智能管家) |
|---|---|---|
| 灵活性 | 固定时间,死板 | 动态调整,智能响应 |
| 实时性 | 可能延迟(被阻塞) | 按优先级处理 |
| 资源占用 | 频繁中断,效率低 | 按需执行,更高效 |
| 适用场景 | 简单、低复杂度任务 | 复杂、多任务系统 |
更多推荐



所有评论(0)