第二讲:裸机调度器
基本概念
任务调度器是嵌入式系统中的"时间管理大师",它按预定的时间间隔安排不同的任务执行
| 优势 | 说明 | 实际效果 |
|---|---|---|
| 毫秒级任务调度 | LED以500ms精确闪烁 | |
| 不同频率任务共存 | 按键10ms扫描 + 屏幕100ms刷新 | |
避免Delay()卡死 |
系统永远响应及时 | |
| 任务独立开发 | 方便团队协作维护 | |
| 任务执行时间可控 | 满足实时性要求 |
结构体基础
1.
结构体变量声明与初始化
/* 方法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.
访问结构体成员
可以使用点运算符(.)访问结构体成员,或使用箭头运算符(->)访问结构体指针的成员
// 创建任务实例
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.
结构体指针(调度器核心!)
结构体指针在任务调度器中扮演重要角色,尤其是在任务队列管理和动态分配任务时
// 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.
结构体嵌套(复杂系统必备)
结构体可以嵌套其他结构体作为成员,这在复杂系统中非常有用,如任务调度器中定义任务组和依赖关系
// 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.
结构体数组
结构体数组是实现任务队列的重要手段,可以批量管理多个任务,便于遍历和调度
// 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. 任务数组定义![]()
// 全局变量,用于存储任务数量
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.初始化函数![]()
//计算任务数组的元素个数,并将结果存储在 task_num 中
void scheduler_init(void)
{
// 计算任务数组的元素个数,并将结果存储在 task_num 中
task_num = sizeof(scheduler_task) / sizeof(scheduler_task_t);
}
初始化函数非常简单,它计算任务数组中的任务数量,并存储在全局变量 task_num 中。
这里使用了一个常见技巧:通过数组总大小除以单个元素大小,得到数组元素个数。
任务数量 = 数组总大小 ÷ 单个任务大小
3.运行函数![]()
//遍历任务数组,检查是否有任务需要执行。如果当前时间已经超过任务的执行周期,则执行该任务并更新上次运行时间
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();
}
}
}
运行函数是调度器的核心,它负责检查并执行满足条件的任务
调度器进阶应用
优先级调度
为任务添加优先级属性,使重要任务优先执行。在某些场景下,我们需要确保关键任务能够及时响应,如安全监控、通信处理等
常见的有:冒泡排序(简单但低效),每次查找最高优先级(更常用),固定优先级抢占(像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;
}
}
}
}
低功耗管理
通过调度器实现低功耗模式的切换与管理。在电池供电的设备中,电源管理至关重要。当没有任务需要立即执行时,系统可以进入低功耗模式以延长电池寿命
低功耗管理实现复制代码
// 在调度器运行函数中添加低功耗管理
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);//睡眠模式
}
}
动态任务管理
在运行时添加、删除或修改任务的能力。静态任务数组在某些场景下可能不够灵活,比如根据系统状态需要动态增减任务时。动态任务管理使调度器更加灵活
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;
}
调度器优化技巧
时间溢出处理![]()
由于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(); // 低优先级任务
}
裸机开发调度器开发的对比
| 维度 | 裸机开发 (Bare-Metal) |
调度器开发 (Scheduler-Based) |
生活比喻 |
|---|---|---|---|
| 架构 | 单任务顺序执行 | 多任务并行调度 | 单车道公路 |
| 控制方式 | 直接操作寄存器/硬件 | 通过调度器管理任务 | 手工做菜 |
| 资源消耗 | CPU占用率100%忙等待 | 有休眠机制,可低功耗 | 灯泡常亮 |
| 执行模式 | while(1)轮询 |
任务调度+事件驱动 | 不停敲门问 |
| 任务管理 | 手动时序编排 | 自动优先级调度 | 一人管所有 |
| 响应延迟 | 取决于当前任务 | 可抢占式立即响应 | 排队结账 |