第二讲:裸机调度器

第二讲:裸机调度器

基本概念

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

优势 说明 实际效果
:stopwatch: 精准定时 毫秒级任务调度 LED以500ms精确闪烁
:counterclockwise_arrows_button: 周期执行 不同频率任务共存 按键10ms扫描 + 屏幕100ms刷新
:high_voltage: 非阻塞运行 避免Delay()卡死 系统永远响应及时
:puzzle_piece: 模块化设计 任务独立开发 方便团队协作维护
:bar_chart: 可预测性 任务执行时间可控 满足实时性要求

结构体基础

1.:building_construction: 结构体变量声明与初始化

/* 方法1:传统方式 */
struct Task {                    // struct Task 是完整的类型名
    char name[32];              // 任务名称
    int priority;              // 优先级
};
struct Task task1 = {"温度检测", 1};  // 声明task1变量并初始化

/* 方法2:typedef别名(推荐🌟) */
typedef struct {                // 定义结构体类型并创建别名,注意:这里没有结构体标签名
    char name[32];
    int priority;
    void (*function)(void);     // 函数指针:指向任务函数
} Task_t;                       // Task_t 就是类型别名
Task_t task2 = {"湿度检测", 2, NULL};  // 使用别名声明

/* 方法3:C99指定初始化(清晰明了) */
Task_t task3 = {
    .name = "压力检测",         // 明确指定成员初始化
    .priority = 3,
    .function = pressure_task   // 指向具体函数
};

2.:magnifying_glass_tilted_left: 访问结构体成员

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

// 创建任务实例
Task_t my_task = {"显示刷新", 5, refresh_display};

// 📍 点运算符(.):直接访问
printf("任务: %s\n", my_task.name);      // 读取成员
my_task.priority = 4;                    // 修改成员
my_task.function();                      // 调用函数指针

// 🎯 箭头运算符(->):指针访问
Task_t* p_task = &my_task;               // 获取指针
printf("优先级: %d\n", p_task->priority); // 通过指针访问
p_task->function();                       // 通过指针调用函数

3.:video_game: 结构体指针(调度器核心!)

结构体指针在任务调度器中扮演重要角色,尤其是在任务队列管理和动态分配任务时

// 1. 定义学生结构体
typedef struct {
    int id;
    char name[20];
    float score;
} Student_t;

// 2. 创建结构体指针
Student_t* student_ptr = NULL;  // 声明指针,初始化为NULL

// 3. 指向现有结构体变量
Student_t student1 = {1001, "张三", 85.5};
student_ptr = &student1;  // 指针指向student1的地址

// 4. 通过指针访问成员(使用箭头运算符->)
printf("学号: %d\n", student_ptr->id);     // 等同于 (*student_ptr).id
printf("姓名: %s\n", student_ptr->name);   // 访问姓名
printf("成绩: %.1f\n", student_ptr->score); // 访问成绩

// 5. 通过指针修改成员
student_ptr->score = 90.0;  // 修改成绩
(*student_ptr).id = 1002;   // 另一种写法(不常用)

// 6. 动态创建学生(堆内存分配)
Student_t* dynamic_student = malloc(sizeof(Student_t));  // 动态分配内存
if(dynamic_student != NULL) {
    dynamic_student->id = 2001;
    strcpy(dynamic_student->name, "李四");
    dynamic_student->score = 92.5;
    
    // 使用完后释放内存
    free(dynamic_student);
    dynamic_student = NULL;  // 防止野指针
}

4.:circus_tent: 结构体嵌套(复杂系统必备)

结构体可以嵌套其他结构体作为成员,这在复杂系统中非常有用,如任务调度器中定义任务组和依赖关系

// 1. 嵌套的地址结构体
typedef struct {
    char city[20];    // 城市
    char street[50];  // 街道
    int zip_code;     // 邮编
} Address_t;

// 2. 嵌套的联系方式结构体  
typedef struct {
    char phone[15];   // 电话
    char email[30];   // 邮箱
} Contact_t;

// 3. 主结构体:员工信息(嵌套了上面两个)
typedef struct {
    int id;                  // 员工ID
    char name[30];          // 姓名
    float salary;           // 工资
    Address_t address;      // 嵌套:地址信息
    Contact_t contact;      // 嵌套:联系方式
    bool is_full_time;      // 是否全职
} Employee_t;

// 4. 使用示例
Employee_t emp1 = {
    .id = 1001,
    .name = "张三",
    .salary = 15000.0,
    .address = {
        .city = "北京",
        .street = "中关村大街1号",
        .zip_code = 100080
    },
    .contact = {
        .phone = "13800138000",
        .email = "zhangsan@company.com"
    },
    .is_full_time = true
};

// 5. 访问嵌套成员
printf("员工: %s\n", emp1.name);
printf("城市: %s\n", emp1.address.city);        // 访问嵌套结构体的成员
printf("电话: %s\n", emp1.contact.phone);

5.:bar_chart: 结构体数组

结构体数组是实现任务队列的重要手段,可以批量管理多个任务,便于遍历和调度

// 1. 商品结构体
typedef struct {
    char name[30];    // 商品名称
    float price;      // 单价
    int quantity;     // 数量
    float total;      // 小计 = 单价 × 数量
} Product_t;

// 2. 购物车数组
Product_t cart[10];
int item_count = 0;   // 当前商品数量

// 3. 添加商品到购物车
void add_to_cart(char* name, float price, int qty) {
    if(item_count < 10) {
        cart[item_count].name = name;
        cart[item_count].price = price;
        cart[item_count].quantity = qty;
        cart[item_count].total = price * qty;
        item_count++;
    }
}

// 4. 计算购物车总价
float calculate_total() {
    float sum = 0;
    for(int i = 0; i < item_count; i++) {
        sum += cart[i].total;
    }
    return sum;
}

调度器实现

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

1. 任务数组定义:package:

// 全局变量,用于存储任务数量
uint8_t task_num;

// 步骤1:先定义结构体类型
typedef struct {
    void (*task_func)(void);   // 函数指针成员
    uint32_t period_ms;        // 周期(毫秒)
    uint32_t last_run;         // 上次执行时间
} scheduler_task_t;

// 步骤2:声明并初始化结构体数组
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,运行时会被更新

2.初始化函数:wrench:

//计算任务数组的元素个数,并将结果存储在 task_num 中
void scheduler_init(void)
{
    // 计算任务数组的元素个数,并将结果存储在 task_num 中
    task_num = sizeof(scheduler_task) / sizeof(scheduler_task_t);
}

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

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

任务数量 = 数组总大小 ÷ 单个任务大小

3.运行函数:person_running:

//遍历任务数组,检查是否有任务需要执行。如果当前时间已经超过任务的执行周期,则执行该任务并更新上次运行时间
void scheduler_run(void)
{
    // 遍历任务数组中的所有任务
    for (uint8_t i = 0; i < task_num; i++)
    {
        // 获取当前的系统时间(毫秒),调用 HAL_GetTick() 获取系统当前时间戳
        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();
        }
    }
}

运行函数是调度器的核心,它负责检查并执行满足条件的任务

调度器进阶应用

:alarm_clock:优先级调度

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

常见的有:冒泡排序(简单但低效),每次查找最高优先级(更常用),固定优先级抢占(像RTOS) 等等

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;
            }
        }
    }
}

:battery:低功耗管理

通过调度器实现低功耗模式的切换与管理。在电池供电的设备中,电源管理至关重要。当没有任务需要立即执行时,系统可以进入低功耗模式以延长电池寿命

低功耗管理实现复制代码

// 在调度器运行函数中添加低功耗管理
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;
        // 情况A:时间到了(time_to_task == 0)                     
        if (time_to_task == 0) {
            // 有任务需要立即执行
            scheduler_task[i].last_run = now_time;
            scheduler_task[i].task_func();
            all_tasks_idle = false;// 🚨 有任务执行,系统不空闲!
        } 
        // 情况B:还没到时间,但需要记录最近的任务
        else if (time_to_task < time_to_next_task) {
            // 记录最近需要执行的任务时间
            time_to_next_task = time_to_task;
        }
    }
    // 条件1:所有任务都空闲吗?
	// 条件2:距离下一个任务是否足够长?
    // 如果所有任务都不需要立即执行,进入低功耗模式
    if (all_tasks_idle && time_to_next_task > MIN_SLEEP_TIME) {
        //满足条件,进入低功耗模式直到下一个任务时间或外部中断
        HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);//睡眠模式
    }
}

:counterclockwise_arrows_button: 动态任务管理

在运行时添加、删除或修改任务的能力。静态任务数组在某些场景下可能不够灵活,比如根据系统状态需要动态增减任务时。动态任务管理使调度器更加灵活

typedef struct {
    void (*task_func)(void);
    uint32_t rate_ms;
    uint32_t last_run;
    bool active;  // 激活状态(新增!)
} dynamic_task_t;

#define MAX_TASKS 10// 最大任务数(容量上限)
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; // 任务已满,返回错误码(255表示失败)
    }
    
    // 📝 填写新任务的信息
    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;
    
    // 📈 返回新任务的ID,并增加任务计数
    return task_num++;
}

// 删除任务
bool remove_task(uint8_t task_id)
{
    // 🔒 安全检查:任务ID是否有效?
    if (task_id >= task_num) {
        return false;// 无效ID,删除失败
    }
    
    // 🔄 数组压缩:移动后面的元素填补空缺
    // 原理:把被删除位置后面的所有任务往前移动一格
    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;
}

调度器优化技巧

时间溢出处理:abacus:

由于32位计数器最终会溢出,确保您的时间比较逻辑能处理时间戳溢出的情况,把时间差当作有符号数,负的表示"还没到时间",正的表示"时间到了或超时”。

// 处理时间溢出的比较方法
if ((int32_t)(now_time - (scheduler_task[i].last_run + scheduler_task[i].rate_ms)) >= 0) {
    // 执行任务
}

任务执行时间监控:police_car_light:****

监控任务执行时间,确保没有任务占用过多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();  // 低优先级任务
}

裸机开发调度器开发的对比

维度 裸机开发 (Bare-Metal) :hammer_and_wrench: 调度器开发 (Scheduler-Based) :vertical_traffic_light: 生活比喻 :cityscape:
架构 单任务顺序执行 多任务并行调度 单车道公路 :vs_button: 多车道高速公路
控制方式 直接操作寄存器/硬件 通过调度器管理任务 手工做菜 :vs_button: 餐厅点餐系统
资源消耗 CPU占用率100%忙等待 有休眠机制,可低功耗 灯泡常亮 :vs_button: 声控感应灯
执行模式 while(1)轮询 任务调度+事件驱动 不停敲门问 :vs_button: 按门铃等回复
任务管理 手动时序编排 自动优先级调度 一人管所有 :vs_button: 团队分工协作
响应延迟 取决于当前任务 可抢占式立即响应 排队结账 :vs_button: 急诊优先
1 个赞