1. 项目概述
在嵌入式系统开发中,任务调度器是一个核心组件,它决定了系统如何高效地管理和执行多个任务。对于参加蓝桥杯嵌入式竞赛的开发者来说,掌握STM32平台上的任务调度实现尤为重要。本文将详细介绍一个基于STM32的轻量级任务调度器实现方案,这个方案特别适合资源受限的MCU环境。
这个调度器的核心思想是通过时间片轮询的方式,在单个主循环中按预定频率执行不同任务。相比复杂的RTOS系统,这种实现方式更加轻量,不需要额外的内存开销,特别适合蓝桥杯嵌入式竞赛中对实时性和资源占用有严格要求的场景。
2. 任务调度器设计原理
2.1 调度器基本结构
任务调度器的核心是一个任务表(scheduler_t),其中每个任务包含三个关键信息:
- 任务函数指针(task_func):指向需要执行的函数
- 执行周期(rate_ms):任务两次执行之间的最小时间间隔(毫秒)
- 上次执行时间(last_run):记录任务最后一次执行的时间戳
c复制typedef struct {
void(*task_func)(void);
uint32_t rate_ms;
uint32_t last_run;
} task_t;
这种设计允许我们通过简单的比较当前时间与上次执行时间+周期,来决定是否执行某个任务。
2.2 调度算法解析
调度器采用非抢占式的时间片轮询算法,其工作流程如下:
- 初始化时计算任务数量
- 在主循环中不断检查各个任务的执行条件
- 当满足条件(当前时间 ≥ 上次执行时间 + 周期)时执行任务
- 更新该任务的上次执行时间为当前时间
c复制void task_run() {
for(uint8_t i=0; i<task_num; i++) {
uint32_t time_tick = uwTick;
if(time_tick >= scheduler_t[i].rate_ms + scheduler_t[i].last_run) {
scheduler_t[i].last_run = time_tick;
scheduler_t[i].task_func();
}
}
}
注意:uwTick是STM32 HAL库提供的系统时间戳,通常由SysTick中断自动更新,每毫秒递增1。
2.3 与RTOS的对比
相比完整的RTOS系统,这个调度器有以下特点:
| 特性 | 本调度器 | RTOS |
|---|---|---|
| 任务优先级 | 无(顺序执行) | 支持多优先级 |
| 任务抢占 | 不支持 | 支持 |
| 上下文切换 | 无 | 有 |
| 内存占用 | 极小 | 较大 |
| 适用场景 | 简单周期性任务 | 复杂多任务系统 |
3. 具体实现步骤
3.1 文件结构搭建
首先需要创建两个关键文件:
task.h- 声明调度器接口task.c- 实现调度器核心逻辑
建议的文件结构如下:
code复制Project/
├── Core/
│ ├── Inc/
│ │ └── task.h
│ └── Src/
│ └── task.c
└── ...
3.2 task.h 实现
头文件主要声明调度器的公共接口:
c复制#ifndef _TASK_H
#define _TASK_H
#include "main.h"
#include <stdint.h>
void task_init(void);
void task_run(void);
#endif
3.3 task.c 实现
核心实现文件包含以下几个部分:
- 任务结构体定义
- 任务表定义
- 初始化函数
- 调度执行函数
完整实现如下:
c复制#include "task.h"
uint8_t task_num; // 记录任务数量
// 任务结构体定义
typedef struct {
void(*task_func)(void); // 任务函数指针
uint32_t rate_ms; // 执行周期(ms)
uint32_t last_run; // 上次执行时间
} task_t;
// 任务表 - 在此添加需要调度的任务
task_t scheduler_t[] = {
// 示例:{task_function, 1000, 0} 表示每1000ms执行一次task_function
};
// 初始化任务数量
void task_init() {
task_num = sizeof(scheduler_t) / sizeof(task_t);
}
// 任务调度执行函数
void task_run() {
for(uint8_t i=0; i<task_num; i++) {
uint32_t time_tick = uwTick;
if(time_tick >= scheduler_t[i].rate_ms + scheduler_t[i].last_run) {
scheduler_t[i].last_run = time_tick;
scheduler_t[i].task_func();
}
}
}
3.4 main.c 集成
在主程序中集成调度器的步骤:
- 包含task.h头文件
- 在程序初始化阶段调用task_init()
- 在主循环中不断调用task_run()
c复制#include "task.h"
int main(void) {
// HAL初始化...
/* USER CODE BEGIN 2 */
task_init(); // 初始化任务调度器
/* USER CODE END 2 */
while (1) {
/* USER CODE BEGIN WHILE */
task_run(); // 执行任务调度
/* USER CODE END WHILE */
}
}
重要提示:所有用户代码必须放在USER CODE BEGIN和USER CODE END注释之间,否则使用STM32CubeMX重新生成代码时会被覆盖。
4. 实际应用示例
4.1 定义任务函数
首先定义几个简单的任务函数:
c复制void task1(void) {
// 任务1的具体实现
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
}
void task2(void) {
// 任务2的具体实现
// 例如读取传感器数据等
}
4.2 配置任务表
在task.c中配置任务表:
c复制task_t scheduler_t[] = {
{task1, 500, 0}, // 每500ms执行一次task1
{task2, 1000, 0}, // 每1000ms执行一次task2
// 可以继续添加更多任务...
};
4.3 调试技巧
-
使用Keil调试器:
- 在调试模式下观察uwTick变量的变化
- 设置断点检查各个任务的last_run是否按预期更新
-
IO口调试法:
- 在任务开始和结束时切换GPIO状态
- 用逻辑分析仪观察任务执行时序
-
执行时间测量:
- 在任务函数前后读取uwTick,计算执行时间
- 确保任务执行时间远小于其调度周期
5. 性能优化与注意事项
5.1 任务设计原则
- 保持任务短小:每个任务应尽快执行完毕,避免长时间占用CPU
- 合理设置周期:根据任务重要性设置适当的执行频率
- 避免阻塞调用:不要在任务中使用delay等阻塞函数
5.2 常见问题排查
-
任务未执行:
- 检查任务是否正确定义在scheduler_t数组中
- 确认task_init()被调用且task_num正确
- 验证uwTick是否正常递增
-
任务执行不稳定:
- 检查是否有任务的执行时间超过其周期
- 确认没有其他中断占用过多CPU时间
-
系统卡顿:
- 使用调试器检查任务执行频率
- 考虑减少任务数量或优化任务执行时间
5.3 高级技巧
-
动态任务添加:
可以通过扩展数据结构支持运行时动态添加任务 -
任务优先级模拟:
通过调整任务在表中的顺序和调用频率模拟简单优先级 -
低功耗集成:
在空闲时调用HAL_PWR_EnterSLEEPMode()降低功耗
6. 扩展思考
这个基础调度器可以进一步扩展:
- 任务状态监控:添加任务执行时间统计功能
- 动态周期调整:根据系统负载动态调整任务执行频率
- 任务间通信:添加简单的消息队列实现任务通信
对于更复杂的应用场景,可以考虑迁移到FreeRTOS等成熟RTOS系统,但对于蓝桥杯嵌入式竞赛和大多数简单应用,这个轻量级调度器已经足够使用。