什么是任务调度器?
任务调度器是嵌入式系统中的"时间管理大师",它按预定的时间间隔安排不同的任务执行。
想象一下,你有一位管家,他根据你设定的时间表,精确地提醒你完成各种任务。任务调度器就是你的程序中的这位"数字管家"。
嵌入式系统的时间管理艺术
任务调度器是嵌入式系统中的"时间管理大师",它按预定的时间间隔安排不同的任务执行。
想象一下,你有一位管家,他根据你设定的时间表,精确地提醒你完成各种任务。任务调度器就是你的程序中的这位"数字管家"。
在嵌入式系统中,资源有限,我们需要高效地管理时间。任务调度器让我们能够:
结构体是C语言中用于存储不同类型数据的复合数据类型,是实现任务调度器的基础
结构体是C语言中的一种自定义数据类型,允许程序员将不同类型的相关数据组合到一个单一的变量中。例如,一个任务可能需要名称、优先级、执行函数等数据项。
// 任务结构体
struct Task {
char name[32]; // 任务名称
int priority; // 任务优先级
void (*function)(void*); // 任务函数指针
void* param; // 任务参数
uint32_t period; // 任务周期(毫秒)
uint32_t next_run; // 下次运行时间
bool is_running; // 运行状态
};
结构体在内存中是连续存储的,但可能会因为内存对齐而包含填充位。理解内存布局对优化存储和高效访问结构体成员至关重要。
内存对齐原则:
__attribute__((packed))
关键字取消对齐例如:在32位系统上,int
通常占4字节并按4字节对齐,char
占1字节按1字节对齐。
可以在定义后直接声明变量,也可以先定义结构体类型,再声明结构体变量。
// 方法1:定义类型并声明变量
struct Task {
char name[32];
int priority;
};
struct Task task1 = {"温度检测", 1};
// 方法2:定义类型别名,便于使用
typedef struct {
char name[32];
int priority;
} Task_t;
Task_t task2 = {"湿度检测", 2};
// 方法3:使用指定初始化(C99起)
Task_t task3 = {
.name = "压力检测",
.priority = 3
};
可以使用点运算符(.)访问结构体成员,或使用箭头运算符(->)访问结构体指针的成员。
// 使用点运算符访问成员
Task_t task = {"显示刷新", 5};
printf("任务名称: %s\n", task.name);
task.priority = 4; // 修改优先级
// 使用箭头运算符访问指针成员
Task_t* p_task = &task;
printf("任务优先级: %d\n", p_task->priority);
p_task->priority++; // 增加优先级
结构体指针在任务调度器中扮演重要角色,尤其是在任务队列管理和动态分配任务时。
// 结构体指针示例
Task_t task_array[10]; // 任务数组
Task_t* curr_task; // 当前任务指针
// 将指针指向数组元素
curr_task = &task_array[0];
// 通过指针修改任务状态
curr_task->is_running = true;
// 指针递增,访问下一个任务
curr_task++; // 现在指向task_array[1]
// 结构体指针作为函数参数
void execute_task(Task_t* task) {
if(task->function != NULL) {
task->function(task->param);
}
}
结构体可以嵌套其他结构体作为成员,这在复杂系统中非常有用,如任务调度器中定义任务组和依赖关系。
// 定义时间结构体
typedef struct {
int hour;
int minute;
int second;
} Time_t;
// 定义任务结构体,嵌套时间结构体
typedef struct {
char name[32];
int priority;
Time_t start_time; // 嵌套的时间结构体
Time_t deadline; // 截止时间
} ScheduledTask_t;
// 结构体嵌套初始化
ScheduledTask_t scheduled_task = {
.name = "数据采集",
.priority = 2,
.start_time = {8, 30, 0}, // 8:30:00
.deadline = {9, 0, 0} // 9:00:00
};
// 访问嵌套结构体成员
printf("任务开始时间: %02d:%02d:%02d\n",
scheduled_task.start_time.hour,
scheduled_task.start_time.minute,
scheduled_task.start_time.second);
结构体数组是实现任务队列的重要手段,可以批量管理多个任务,便于遍历和调度。
// 定义任务结构体数组
Task_t task_queue[MAX_TASKS];
// 初始化任务队列
void init_task_queue() {
for(int i = 0; i < MAX_TASKS; i++) {
strcpy(task_queue[i].name, "空闲");
task_queue[i].priority = 0;
task_queue[i].function = NULL;
task_queue[i].is_running = false;
}
}
// 添加任务到队列
bool add_task(Task_t* new_task) {
for(int i = 0; i < MAX_TASKS; i++) {
if(task_queue[i].function == NULL) {
// 找到空闲槽位,复制任务信息
memcpy(&task_queue[i], new_task, sizeof(Task_t));
return true;
}
}
return false; // 队列已满
}
// 按优先级排序任务队列
void sort_task_queue() {
for(int i = 0; i < MAX_TASKS-1; i++) {
for(int j = 0; j < MAX_TASKS-i-1; j++) {
if(task_queue[j].priority < task_queue[j+1].priority) {
// 交换任务位置
Task_t temp = task_queue[j];
task_queue[j] = task_queue[j+1];
task_queue[j+1] = temp;
}
}
}
}
任务调度器的核心是一个精心设计的结构体,它定义了每个任务的关键属性。
typedef struct {
void (*task_func)(void); // 任务函数指针
uint32_t rate_ms; // 执行周期(毫秒)
uint32_t last_run; // 上次执行时间
} scheduler_task_t;
函数指针,指向任务的执行函数。这个函数将在调度时被调用。
任务的执行周期,以毫秒为单位。表示该任务应每隔多长时间执行一次。
记录任务上次执行的时间戳。用于计算是否到达下次执行时间。
调度器的实现主要包含三个部分:任务数组、初始化函数和运行函数。
// 全局变量,用于存储任务数量
uint8_t task_num;
// 静态任务数组,每个任务包含任务函数、执行周期(毫秒)和上次运行时间(毫秒)
static scheduler_task_t scheduler_task[] =
{
{Led_Proc, 1, 0}, // LED控制任务:周期1ms
{Key_Proc, 10, 0}, // 按键扫描任务:周期10ms
{Sensor_Proc, 100, 0}, // 传感器读取任务:周期100ms
{Comm_Proc, 50, 0} // 通信处理任务:周期50ms
};
任务数组是调度器的核心,它存储了所有需要被调度的任务。每个任务包含三个元素:
/**
* @brief 调度器初始化函数
* 计算任务数组的元素个数,并将结果存储在 task_num 中
*/
void scheduler_init(void)
{
// 计算任务数组的元素个数,并将结果存储在 task_num 中
task_num = sizeof(scheduler_task) / sizeof(scheduler_task_t);
}
初始化函数非常简单,它计算任务数组中的任务数量,并存储在全局变量 task_num
中。
这里使用了一个常见技巧:通过数组总大小除以单个元素大小,得到数组元素个数。
/**
* @brief 调度器运行函数
* 遍历任务数组,检查是否有任务需要执行。如果当前时间已经超过任务的执行周期,则执行该任务并更新上次运行时间
*/
void scheduler_run(void)
{
// 遍历任务数组中的所有任务
for (uint8_t i = 0; i < task_num; i++)
{
// 获取当前的系统时间(毫秒)
uint32_t now_time = HAL_GetTick();
// 检查当前时间是否达到任务的执行时间
if (now_time >= scheduler_task[i].rate_ms + scheduler_task[i].last_run)
{
// 更新任务的上次运行时间为当前时间
scheduler_task[i].last_run = now_time;
// 执行任务函数
scheduler_task[i].task_func();
}
}
}
运行函数是调度器的核心,它负责检查并执行满足条件的任务。工作流程如下:
循环检查每个任务是否需要执行
调用 HAL_GetTick() 获取系统当前时间戳
比较当前时间是否超过了 (上次执行时间 + 执行周期)
满足条件时,更新上次执行时间并调用任务函数
在前面的代码中,我们多次看到了HAL_GetTick()
等函数的调用。这些是什么呢?它们来自嵌入式开发中广泛使用的HAL库。
HAL(Hardware Abstraction Layer,硬件抽象层)是一种软件接口,它为上层应用程序提供访问硬件的统一方式,屏蔽了不同硬件平台的差异。
在嵌入式系统中,HAL库使开发者能够编写可移植的代码,不必直接操作硬件寄存器。
uint32_t HAL_GetTick(void);
返回自系统启动以来经过的毫秒数。
HAL库通过硬件定时器实现该功能,定时器在后台不断计数,HAL_GetTick函数返回当前的计数值。
在任务调度器中,我们使用HAL_GetTick()
函数获取当前时间,然后与任务的上次执行时间比较,决定是否执行任务。
uint32_t now_time = HAL_GetTick();
if (now_time >= scheduler_task[i].rate_ms + scheduler_task[i].last_run) {
// 执行任务
}
下面是一个完整的调度器实现示例,展示了如何在实际项目中应用任务调度器。
#include "scheduler.h"
// 全局变量,用于存储任务数量
uint8_t task_num;
typedef struct {
void (*task_func)(void);
uint32_t rate_ms;
uint32_t last_run;
} task_t;
// 任务函数声明
void led_proc(void);
void key_proc(void);
void sensor_proc(void);
void comm_proc(void);
// 静态任务数组,每个任务包含任务函数、执行周期(毫秒)和上次运行时间(毫秒)
static task_t scheduler_task[] =
{
{led_proc, 1000, 0}, // LED闪烁任务:周期1000ms (1秒)
{key_proc, 10, 0}, // 按键扫描任务:周期10ms
{sensor_proc, 100, 0}, // 传感器读取任务:周期100ms
{comm_proc, 50, 0} // 通信处理任务:周期50ms
};
/**
* @brief 调度器初始化函数
* 计算任务数组的元素个数,并将结果存储在 task_num 中
*/
void scheduler_init(void)
{
// 计算任务数组的元素个数,并将结果存储在 task_num 中
task_num = sizeof(scheduler_task) / sizeof(task_t);
}
/**
* @brief 调度器运行函数
* 遍历任务数组,检查是否有任务需要执行。如果当前时间已经超过任务的执行周期,则执行该任务并更新上次运行时间
*/
void scheduler_run(void)
{
// 遍历任务数组中的所有任务
for (uint8_t i = 0; i < task_num; i++)
{
// 获取当前的系统时间(毫秒)
uint32_t now_time = HAL_GetTick();
// 检查当前时间是否达到任务的执行时间
if (now_time >= scheduler_task[i].rate_ms + scheduler_task[i].last_run)
{
// 更新任务的上次运行时间为当前时间
scheduler_task[i].last_run = now_time;
// 执行任务函数
scheduler_task[i].task_func();
}
}
}
// 任务函数实现
void led_proc(void)
{
// 切换LED状态
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
void key_proc(void)
{
// 读取按键状态
if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET)
{
// 按键被按下,执行相应操作
}
}
void sensor_proc(void)
{
// 读取传感器数据
uint16_t sensor_value = HAL_ADC_GetValue(&hadc1);
// 处理传感器数据
}
void comm_proc(void)
{
// 处理通信数据
if (HAL_UART_GetState(&huart1) == HAL_UART_STATE_READY)
{
// 发送数据
}
}
任务调度器的应用远不止于此,下面是一些进阶技巧和应用场景。
为任务添加优先级属性,使重要任务优先执行。在某些场景下,我们需要确保关键任务能够及时响应,如安全监控、通信处理等。
typedef struct {
void (*task_func)(void);
uint32_t rate_ms;
uint32_t last_run;
uint8_t priority; // 优先级,数值越小优先级越高
} priority_task_t;
// 任务按优先级排序函数
void sort_tasks_by_priority(void) {
// 使用冒泡排序按优先级排序
for (uint8_t i = 0; i < task_num - 1; i++) {
for (uint8_t j = 0; j < task_num - i - 1; j++) {
if (scheduler_task[j].priority > scheduler_task[j + 1].priority) {
// 交换任务
priority_task_t temp = scheduler_task[j];
scheduler_task[j] = scheduler_task[j + 1];
scheduler_task[j + 1] = temp;
}
}
}
}
通过调度器实现低功耗模式的切换与管理。在电池供电的设备中,电源管理至关重要。当没有任务需要立即执行时,系统可以进入低功耗模式以延长电池寿命。
// 在调度器运行函数中添加低功耗管理
void scheduler_run(void)
{
bool all_tasks_idle = true;
uint32_t time_to_next_task = UINT32_MAX;
uint32_t now_time = HAL_GetTick();
// 检查是否有任务需要立即执行
for (uint8_t i = 0; i < task_num; i++)
{
uint32_t time_to_task = (scheduler_task[i].last_run +
scheduler_task[i].rate_ms) - now_time;
if (time_to_task == 0) {
// 有任务需要立即执行
scheduler_task[i].last_run = now_time;
scheduler_task[i].task_func();
all_tasks_idle = false;
} else if (time_to_task < time_to_next_task) {
// 记录最近需要执行的任务时间
time_to_next_task = time_to_task;
}
}
// 如果所有任务都不需要立即执行,进入低功耗模式
if (all_tasks_idle && time_to_next_task > MIN_SLEEP_TIME) {
// 进入低功耗模式直到下一个任务时间或外部中断
HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);
}
}
在运行时添加、删除或修改任务的能力。静态任务数组在某些场景下可能不够灵活,比如根据系统状态需要动态增减任务时。动态任务管理使调度器更加灵活。
#define MAX_TASKS 10
typedef struct {
void (*task_func)(void);
uint32_t rate_ms;
uint32_t last_run;
bool active; // 任务是否激活
} dynamic_task_t;
dynamic_task_t scheduler_task[MAX_TASKS];
uint8_t task_num = 0;
// 添加任务
uint8_t add_task(void (*task_func)(void), uint32_t rate_ms) {
if (task_num >= MAX_TASKS) {
return 0xFF; // 任务已满
}
scheduler_task[task_num].task_func = task_func;
scheduler_task[task_num].rate_ms = rate_ms;
scheduler_task[task_num].last_run = HAL_GetTick();
scheduler_task[task_num].active = true;
return task_num++;
}
// 删除任务
bool remove_task(uint8_t task_id) {
if (task_id >= task_num) {
return false;
}
// 移动数组元素以填补空缺
for (uint8_t i = task_id; i < task_num - 1; i++) {
scheduler_task[i] = scheduler_task[i + 1];
}
task_num--;
return true;
}
// 暂停任务
bool pause_task(uint8_t task_id) {
if (task_id >= task_num) {
return false;
}
scheduler_task[task_id].active = false;
return true;
}
// 恢复任务
bool resume_task(uint8_t task_id) {
if (task_id >= task_num) {
return false;
}
scheduler_task[task_id].active = true;
scheduler_task[task_id].last_run = HAL_GetTick();
return true;
}
由于32位计数器最终会溢出,确保您的时间比较逻辑能处理时间戳溢出的情况。在长时间运行的系统中,这一点尤为重要。
// 处理时间溢出的比较方法
if ((int32_t)(now_time - (scheduler_task[i].last_run + scheduler_task[i].rate_ms)) >= 0) {
// 执行任务
}
监控任务执行时间,确保没有任务占用过多CPU时间。这对于实时系统尤为重要,一个任务执行时间过长可能会影响其他任务的及时性。
uint32_t start_time = HAL_GetTick();
scheduler_task[i].task_func();
uint32_t execution_time = HAL_GetTick() - start_time;
if (execution_time > MAX_TASK_TIME) {
// 记录或处理任务执行时间过长的情况
}
可以创建多个不同优先级或时间精度的调度器,以适应不同类型的任务。这种方法允许为不同时间尺度的任务提供最佳的性能和响应性。
// 快速调度器 - 1ms精度的任务
void fast_scheduler_run(void);
// 慢速调度器 - 100ms精度的任务
void slow_scheduler_run(void);
// 在主循环中
while (1) {
fast_scheduler_run(); // 高优先级任务
slow_scheduler_run(); // 低优先级任务
}