在STM32裸机开发中,我们经常需要处理多个周期性任务(比如LED闪烁、按键扫描、传感器采集、串口通信)。传统的轮询方式(死循环里依次调用所有任务)很容易出现"任务阻塞"问题------比如某个任务执行时间稍长,就会导致其他任务的执行周期被打乱。
今天给大家分享一个轻量级任务调度器(阉割版RTOS),它介于裸机和完整RTOS之间,无需移植复杂的操作系统,就能实现多任务的定时、非阻塞执行,非常适合资源有限、实时性要求高的STM32项目!
一、任务调度器核心原理
调度器的核心逻辑极其简洁:
将每个任务的「执行函数、执行周期、上次执行时间」封装为结构体,依托STM32 HAL库的HAL_GetTick()获取毫秒级系统时间,周期性遍历所有任务,判断是否到达执行时间;若到达则执行任务,并更新"上次执行时间",确保任务按设定周期非阻塞运行。
核心优势对比传统轮询:
| 特性 | 传统轮询 | 轻量级调度器 |
|---|---|---|
| 任务阻塞 | 极易阻塞 | 非阻塞(单任务耗时<<周期) |
| 周期精度 | 受任务耗时影响 | 毫秒级精准 |
| 维护性 | 代码耦合度高 | 模块化易维护 |
| 灵活性 | 静态固定 | 可动态调整周期 |
二、核心组件拆解
2.1 任务结构体:封装任务核心信息
首先定义任务结构体,把每个任务的关键信息打包,这是调度器的核心载体:
c
typedef struct {
void (*task_func)(void); // 任务执行函数(无参数)
uint32_t rate_ms; // 执行周期(毫秒)
uint32_t last_run; // 上次执行时间(系统时间戳)
} scheduler_task_t;
task_func:指向任务的具体执行函数(比如LED闪烁、按键扫描);rate_ms:任务的执行周期(比如10ms执行一次按键扫描);last_run:记录任务上次执行的系统时间,用于判断是否该执行任务。
2.2 任务数组:管理所有待调度任务
把所有需要调度的任务存入数组,形成"任务列表",后续遍历数组即可管理所有任务:
c
// 任务数组(添加/删除任务只需修改此处)
static scheduler_task_t scheduler_task[] = {
{led_proc, 1000, 0}, // LED闪烁:1秒执行1次
{key_proc, 10, 0}, // 按键扫描:10ms执行1次
{sensor_proc, 100, 0}, // 传感器采集:100ms执行1次
{comm_proc, 50, 0} // 串口通信:50ms执行1次
};
// 任务数量(全局变量,后续自动计算)
uint8_t task_num;
说明:last_run初始化为0,运行时会被动态更新为任务执行时的系统时间。
2.3 初始化函数:自动计算任务数量
初始化时自动计算任务数组的长度(总任务数),避免手动修改数值导致错误:
c
// 调度器初始化函数
void scheduler_init(void)
{
// 任务数量 = 数组总大小 / 单个任务结构体大小
task_num = sizeof(scheduler_task) / sizeof(scheduler_task_t);
}
2.4 调度核心函数:判断+执行任务
这是调度器的"大脑",遍历所有任务并判断执行时机,核心逻辑只有几行:
c
// 调度器核心运行函数
void scheduler_run(void) {
// 遍历所有任务
for (uint8_t i = 0; i < task_num; i++) {
// 获取当前系统时间(毫秒级,HAL库自带)
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();
}
}
}
⚠️ 核心注意点 :必须先更新last_run再执行任务!
如果先执行任务,last_run会被设为"任务执行结束时间",导致实际周期 = 设定周期 + 任务执行耗时;先更新时间能保证周期严格等于rate_ms(前提是任务执行时间远小于周期)。
三、完整代码实现(可直接复制使用)
3.1 头文件:scheduler.h
c
#ifndef __SCHEDULER_H
#define __SCHEDULER_H
#include "stm32f1xx_hal.h" // 根据实际芯片修改(如stm32f4xx_hal.h)
// 任务结构体定义
typedef struct {
void (*task_func)(void); // 任务执行函数
uint32_t rate_ms; // 执行周期(毫秒)
uint32_t last_run; // 上次执行时间戳
} scheduler_task_t;
// 函数声明
void scheduler_init(void); // 调度器初始化
void scheduler_run(void); // 调度器运行
#endif // __SCHEDULER_H
3.2 源文件:scheduler.c
c
#include "scheduler.h"
// 任务数量(全局变量)
uint8_t task_num;
// 任务函数声明(需根据实际需求实现)
void led_proc(void); // LED控制任务
void key_proc(void); // 按键扫描任务
void sensor_proc(void); // 传感器采集任务
void comm_proc(void); // 串口通信任务
// 任务数组:核心配置区,按需增删任务
static scheduler_task_t scheduler_task[] = {
{led_proc, 1000, 0}, // LED闪烁:1秒1次
{key_proc, 10, 0}, // 按键扫描:10ms1次
{sensor_proc, 100, 0}, // 传感器采集:100ms1次
{comm_proc, 50, 0} // 串口通信:50ms1次
};
// 调度器初始化:计算任务总数
void scheduler_init(void)
{
task_num = sizeof(scheduler_task) / sizeof(scheduler_task_t);
}
// 调度器核心运行函数
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(); // 执行任务
}
}
}
// ------------------- 任务函数示例实现 -------------------
// LED闪烁(需先初始化LED引脚)
void led_proc(void) {
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
// 按键扫描(需先初始化按键引脚,简单消抖示例)
void key_proc(void) {
if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET) {
HAL_Delay(20); // 简易消抖(实际建议用无阻塞消抖)
if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET) {
// 按键按下后的业务逻辑(如切换模式、控制外设)
}
}
}
// 传感器采集(以12位ADC为例,需先初始化ADC)
void sensor_proc(void) {
uint16_t adc_value = HAL_ADC_GetValue(&hadc1); // 读取ADC原始值
float voltage = (float)adc_value * 3.3 / 4096; // 转换为实际电压(3.3V参考)
// 可添加滤波、阈值判断等逻辑
}
// 串口通信(需先初始化UART)
void comm_proc(void) {
uint8_t send_data[] = "STM32 Scheduler Test\r\n";
if (HAL_UART_GetState(&huart1) == HAL_UART_STATE_READY) {
HAL_UART_Transmit(&huart1, send_data, sizeof(send_data)-1, 100);
}
}
四、实战集成:main函数中使用调度器
只需两步,即可在STM32项目中集成调度器:
c
#include "main.h"
#include "scheduler.h"
int main(void) {
// 1. 基础初始化(HAL库+系统时钟+外设)
HAL_Init();
SystemClock_Config(); // STM32CubeMX自动生成
MX_GPIO_Init(); // LED、按键引脚初始化
MX_ADC1_Init(); // ADC初始化
MX_USART1_UART_Init();// 串口初始化
// 2. 调度器初始化
scheduler_init();
// 3. 主循环:仅需调用调度器核心函数
while (1) {
scheduler_run(); // 调度器自动管理所有任务
// 主循环无需额外延时/逻辑,极简!
}
}
五、关键注意事项 & 优化建议
5.1 必看注意事项
- 任务耗时限制:单个任务执行时间必须远小于其周期(比如10ms周期的任务,执行时间需<1ms),否则会导致其他任务延迟;
- 时间戳精度 :
HAL_GetTick()默认基于SysTick定时器,精度为1ms,若需更高精度(如100us),可自定义定时器中断实现时间戳; - 禁用阻塞函数 :任务中避免使用
HAL_Delay()等阻塞函数,建议用时间戳实现无阻塞延时。
5.2 进阶优化方向
- 任务状态控制 :给结构体增加
uint8_t enable字段,支持动态启用/禁用任务; - 优先级支持 :增加
uint8_t prio字段,遍历任务时先执行高优先级任务; - 中断级调度 :把
scheduler_run()放到定时器中断中执行,彻底解放主循环; - 栈保护:针对复杂任务,添加栈溢出检测,提升稳定性。
六、总结 & 拓展学习
总结
这个轻量级调度器是STM32裸机开发的"性价比之王":
- ✅ 非阻塞:单个任务不影响其他任务周期;
- ✅ 低资源占用:无额外内存开销,适配F103C8T6等小资源芯片;
- ✅ 易维护:任务模块化,增删改仅需修改任务数组;
- ✅ 易拓展:可快速升级为支持优先级、动态启停的复杂调度器。
拓展学习
掌握本调度器后,可进一步深入:
- 完整RTOS:学习FreeRTOS/uC/OS的任务调度、内存管理、信号量/队列;
- 多核调度:针对STM32H7等多核芯片,实现核间任务调度;
- 实时性优化:结合DWT定时器实现微秒级高精度调度。
如果这篇文章对你有帮助,欢迎点赞+收藏+关注!有任何问题(代码适配、优化思路),评论区留言,我会第一时间回复~