任务调度器

嵌入式系统的时间管理艺术

什么是任务调度器?

任务调度器是嵌入式系统中的"时间管理大师",它按预定的时间间隔安排不同的任务执行。

想象一下,你有一位管家,他根据你设定的时间表,精确地提醒你完成各种任务。任务调度器就是你的程序中的这位"数字管家"。

早餐
作业
午餐
锻炼
晚餐
睡觉

基本概念

在嵌入式系统中,资源有限,我们需要高效地管理时间。任务调度器让我们能够:

  • ⏱️ 定时执行任务:按照精确的时间间隔运行特定函数
  • 🔄 周期性操作:实现循环执行的功能,如LED闪烁、按键扫描
  • 非阻塞设计:避免使用延时函数导致的系统阻塞
  • 🧩 模块化结构:将不同功能分离为独立任务,提高代码可维护性
LED控制
按键扫描
数据采集
通信处理

C语言结构体基础

结构体是C语言中用于存储不同类型数据的复合数据类型,是实现任务调度器的基础

结构体定义

结构体是C语言中的一种自定义数据类型,允许程序员将不同类型的相关数据组合到一个单一的变量中。例如,一个任务可能需要名称、优先级、执行函数等数据项。

// 任务结构体
struct Task {
    char name[32];      // 任务名称
    int priority;       // 任务优先级
    void (*function)(void*); // 任务函数指针
    void* param;        // 任务参数
    uint32_t period;    // 任务周期(毫秒)
    uint32_t next_run;  // 下次运行时间
    bool is_running;    // 运行状态
};

结构体内存布局

结构体在内存中是连续存储的,但可能会因为内存对齐而包含填充位。理解内存布局对优化存储和高效访问结构体成员至关重要。

0x00 name: char[32] 32B
0x20 priority: int 4B
0x24 function: void(*)(void*) 4/8B
0x28 param: void* 4/8B
0x30 period: uint32_t 4B
0x34 next_run: uint32_t 4B
0x38 is_running: bool 1B
0x39 填充字节: padding 3B

内存对齐原则:

  • 每个结构体成员的地址必须是自身大小的整数倍(或编译器指定的对齐值)
  • 整个结构体的大小必须是所有成员中最大对齐要求的整数倍
  • 可以使用 __attribute__((packed)) 关键字取消对齐

例如:在32位系统上,int 通常占4字节并按4字节对齐,char 占1字节按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
};

2. 访问结构体成员

可以使用点运算符(.)访问结构体成员,或使用箭头运算符(->)访问结构体指针的成员。

// 使用点运算符访问成员
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;

task_func

函数指针,指向任务的执行函数。这个函数将在调度时被调用。

void Led_Proc(void)
{ /* 控制LED闪烁的代码 */ }

rate_ms

任务的执行周期,以毫秒为单位。表示该任务应每隔多长时间执行一次。

1000ms

last_run

记录任务上次执行的时间戳。用于计算是否到达下次执行时间。

5000ms

调度器实现

调度器的实现主要包含三个部分:任务数组、初始化函数和运行函数。

任务数组定义
// 全局变量,用于存储任务数量
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
};

任务数组是调度器的核心,它存储了所有需要被调度的任务。每个任务包含三个元素:

  • 任务函数:当满足执行条件时被调用的函数
  • 执行周期:任务的执行周期(毫秒)
  • 上次运行时间:初始化为0,运行时会被更新
初始化函数
/**
 * @brief 调度器初始化函数
 * 计算任务数组的元素个数,并将结果存储在 task_num 中
 */
void scheduler_init(void)
{
    // 计算任务数组的元素个数,并将结果存储在 task_num 中
    task_num = sizeof(scheduler_task) / sizeof(scheduler_task_t);
}

初始化函数非常简单,它计算任务数组中的任务数量,并存储在全局变量 task_num 中。

这里使用了一个常见技巧:通过数组总大小除以单个元素大小,得到数组元素个数。

任务数量 = 数组总大小 ÷ 单个任务大小
任务1
任务2
任务3
任务4
运行函数
/**
 * @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();
        }
    }
}

运行函数是调度器的核心,它负责检查并执行满足条件的任务。工作流程如下:

  1. 1
    遍历任务数组

    循环检查每个任务是否需要执行

  2. 2
    获取当前时间

    调用 HAL_GetTick() 获取系统当前时间戳

  3. 3
    检查执行条件

    比较当前时间是否超过了 (上次执行时间 + 执行周期)

  4. 4
    执行任务

    满足条件时,更新上次执行时间并调用任务函数

HAL库简介

在前面的代码中,我们多次看到了HAL_GetTick()等函数的调用。这些是什么呢?它们来自嵌入式开发中广泛使用的HAL库。

🔌

什么是HAL库?

HAL(Hardware Abstraction Layer,硬件抽象层)是一种软件接口,它为上层应用程序提供访问硬件的统一方式,屏蔽了不同硬件平台的差异。

在嵌入式系统中,HAL库使开发者能够编写可移植的代码,不必直接操作硬件寄存器。

HAL_GetTick() 函数

HAL_GetTick 函数原型
uint32_t HAL_GetTick(void);

功能:

返回自系统启动以来经过的毫秒数。

描述:

  • 该函数由HAL库提供,用于获取系统启动后经过的时间(毫秒)。
  • 它通常用于时间测量、任务调度和延时控制。

使用场景:

  • 定时任务调度:如本调度器代码,用于确定任务是否需要执行。
  • 计算时间间隔:测量两个事件之间的时间差。
  • 延时操作:结合条件语句或其他逻辑实现延时控制。

实现原理:

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)
    {
        // 发送数据
    }
}

实时任务调度模拟器

10x
系统时间: 0 ms
LED闪烁 (1000ms)
等待中
按键扫描 (10ms)
等待中
传感器读取 (100ms)
等待中
通信处理 (50ms)
等待中
系统输出

进阶应用

任务调度器的应用远不止于此,下面是一些进阶技巧和应用场景。

优先级调度

为任务添加优先级属性,使重要任务优先执行。在某些场景下,我们需要确保关键任务能够及时响应,如安全监控、通信处理等。

优先级调度实现
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();  // 低优先级任务
}