【STM32实战】轻量级任务调度器实现

在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 必看注意事项

  1. 任务耗时限制:单个任务执行时间必须远小于其周期(比如10ms周期的任务,执行时间需<1ms),否则会导致其他任务延迟;
  2. 时间戳精度HAL_GetTick()默认基于SysTick定时器,精度为1ms,若需更高精度(如100us),可自定义定时器中断实现时间戳;
  3. 禁用阻塞函数 :任务中避免使用HAL_Delay()等阻塞函数,建议用时间戳实现无阻塞延时。

5.2 进阶优化方向

  1. 任务状态控制 :给结构体增加uint8_t enable字段,支持动态启用/禁用任务;
  2. 优先级支持 :增加uint8_t prio字段,遍历任务时先执行高优先级任务;
  3. 中断级调度 :把scheduler_run()放到定时器中断中执行,彻底解放主循环;
  4. 栈保护:针对复杂任务,添加栈溢出检测,提升稳定性。

六、总结 & 拓展学习

总结

这个轻量级调度器是STM32裸机开发的"性价比之王":

  • ✅ 非阻塞:单个任务不影响其他任务周期;
  • ✅ 低资源占用:无额外内存开销,适配F103C8T6等小资源芯片;
  • ✅ 易维护:任务模块化,增删改仅需修改任务数组;
  • ✅ 易拓展:可快速升级为支持优先级、动态启停的复杂调度器。

拓展学习

掌握本调度器后,可进一步深入:

  1. 完整RTOS:学习FreeRTOS/uC/OS的任务调度、内存管理、信号量/队列;
  2. 多核调度:针对STM32H7等多核芯片,实现核间任务调度;
  3. 实时性优化:结合DWT定时器实现微秒级高精度调度。

如果这篇文章对你有帮助,欢迎点赞+收藏+关注!有任何问题(代码适配、优化思路),评论区留言,我会第一时间回复~

相关推荐
guygg882 小时前
基于霍尔传感器的BLDC控制源码
单片机·嵌入式硬件
ytttr8732 小时前
DSP 28335 CAN总线通信程序
开发语言·stm32·单片机
一枝小雨4 小时前
RISC-V架构sp寄存器 & RISC-V架构下FreeRTOS任务上下文保存与恢复
单片机·架构·嵌入式·risc-v·rtos·内核原理
BW.SU5 小时前
PackagingTool 嵌入式资源打包合并工具
单片机·二进制·嵌入式开发·资源合并软件·图片打包
田甲6 小时前
STM32开发环境迁移实践:从 CubeMX 生成 CMake 工程到 VS Code 编译与调试
stm32·单片机·嵌入式硬件
hoiii1876 小时前
在 STM32F1上读取 BMX055 三轴加速度
stm32·单片机·嵌入式硬件
嵌入式小站7 小时前
STM32 零基础可移植教程 04:按键输入,为什么按下去读到的是 0 或 1
chrome·stm32·嵌入式硬件
三佛科技-187366133977 小时前
BP8522D贴片SOP7,5V150mA高集成度无VCC电容降压型恒压芯片解析
单片机·嵌入式硬件
csg11077 小时前
MSP430F149驱动T8650北斗模块实现短报文通信实战
单片机·嵌入式硬件·物联网·自动化