跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C

STM32 非阻塞多任务调度方案,无需 RTOS

介绍在 STM32 裸机环境下实现多任务调度的方法。通过利用 HAL 库的 SysTick 定时器获取时间戳,构建轻量级任务调度器,替代传统的阻塞式延时(HAL_Delay)。该方案支持非阻塞轮询,确保按键扫描、屏幕刷新、LED 控制等多任务并发执行,提升系统实时性。代码基于结构体封装,易于移植至不同 STM32 系列,并通过 IO 翻转法验证定时精度。

MqEngine发布于 2026/3/22更新于 2026/5/2536 浏览
STM32 非阻塞多任务调度方案,无需 RTOS

开发环境

  • 底层库:STM32Cube MCU Package (HAL 库)
  • 硬件平台:STM32G4xx/F1xx/F4xx (通用)
  • 核心资源:SysTick 滴答定时器(HAL_GetTick)

在嵌入式开发初期,我们习惯在 while(1) 里通过 HAL_Delay() 控制节奏。但随着任务增多(既要扫按键,又要刷屏幕,还要读传感器),传统的阻塞式延时会导致系统卡顿。

为了解决这个问题,在本方案中采用了轻量级任务调度器(Task Scheduler)架构。通过将系统拆分为多个非阻塞的任务(Task),并按照固定的时间片(如 10ms、50ms)轮询执行,从而确保了多任务环境下的实时性与流畅度。在无需 RTOS 的前提下,使用裸机开发最大限度提升了系统的实时响应能力与执行流畅度。

为什么选择 HAL 库实现?

很多从标准库转 HAL 库的朋友可能会问:标准库也能做调度器,为什么要强调 HAL 库?

  1. 零配置成本:在标准库中,你需要手动配置 SysTick_Config,并在 stm32f10x_it.c 的中断服务函数中手动累加全局变量。而 HAL 库在初始化时默认就开启了 SysTick,并提供了全局可调用的 HAL_GetTick() 函数,真正实现了'开箱即用'。
  2. 跨平台兼容:本套调度器代码在 HAL 库下具有极强的移植性。无论你从 F1 系列换到 G4 甚至 H7 系列,代码逻辑无需改动,只需要更换一下头文件即可。
  3. 更安全的时间戳:HAL 库内置了对 Systick 的管理,避免了开发者重复定义计时器导致的逻辑冲突。

核心原理

传统的 HAL_Delay() 就像是原地睡觉,睡醒前什么都干不了。而调度器逻辑就像是定闹钟打卡:

  1. CPU 不断巡逻:通过 while(1) 极速轮询。
  2. 时间戳比对:利用 HAL 库自带的 HAL_GetTick()(每 1ms 加 1)获取当前时间。
  3. 到点执行:如果'当前时间 - 上次执行时间 >= 设定周期',则触发任务函数。

代码封装

为了提高复用性,我们将调度器逻辑拆分为 scheduler.h 和 scheduler.c。

首先是结构体定义 scheduler.h,我们定义一个任务'身份证',包含它要做什么、多久做一次、上次什么时候做的。

typedef struct {
    void (*pTaskFunc)(void); // 函数指针:要执行的任务
    uint32_t interval;       // 任务周期 (ms)
    uint32_t lastTick;       // 上次执行的时间戳
} Task_t;

其次是调度引擎的实现 scheduler.c,核心算法在于无符号数减法。即便 HAL_GetTick() 在运行 49.7 天后溢出归零,current - last 的结果依然能保持正确。

void scheduler_run(void) {
    uint32_t currentTick = HAL_GetTick();
    for (uint16_t i = 0; i < taskCount; i++) {
        // 时间到!执行并更新时间戳
        if (currentTick - pTaskList[i].lastTick >= pTaskList[i].interval) {
            pTaskList[i].pTaskFunc();
            pTaskList[i].lastTick = currentTick;
        }
    }
}

完整代码如下:

  • scheduler.h
#ifndef __SCHEDULER_H
#define __SCHEDULER_H
// 注意:请根据您的芯片型号修改此头文件(例如 F1 系列为 stm32f1xx_hal.h)
#include "stm32g4xx_hal.h"

// 任务结构体定义
typedef struct {
    void (*pTaskFunc)(void); // 任务函数指针
    uint32_t interval;       // 执行周期 (ms)
    uint32_t lastTick;       // 上次执行时间
} Task_t;

// API 接口声明
void scheduler_init(Task_t* tasks, uint16_t count);
void scheduler_run(void);
#endif
  • scheduler.c
#include "scheduler.h"

static Task_t* pTaskList = NULL;
static uint16_t taskCount = 0;

/**
 * @brief 初始化调度器
 * @param tasks 任务数组首地址
 * @param count 任务总数
 */
void scheduler_init(Task_t* tasks, uint16_t count) {
    pTaskList = tasks;
    taskCount = count;
    // 初始化所有任务的时间戳
    uint32_t currentTick = HAL_GetTick();
    for (uint16_t i = 0; i < taskCount; i++) {
        pTaskList[i].lastTick = currentTick;
    }
}

/**
 * @brief 调度器核心,在 main 循环中调用
 */
void scheduler_run(void) {
    if (pTaskList == NULL) return;
    uint32_t currentTick = HAL_GetTick();
    for (uint16_t i = 0; i < taskCount; i++) {
        // 使用无符号减法,自动处理 Systick 溢出问题
        if (currentTick - pTaskList[i].lastTick >= pTaskList[i].interval) {
            pTaskList[i].pTaskFunc(); // 执行任务
            pTaskList[i].lastTick = currentTick;
        }
    }
}

使用案例

假设我们需要同时处理:20ms 的按键扫描、200ms 的屏幕刷新、1ms 的 LED 控制。

// 1. 定义任务列表
Task_t myTasks[] = {
    {Key_Proc, 20, 0},   // 20ms 扫一次,防抖稳如狗
    {LCD_Proc, 200, 0},  // 200ms 刷屏,肉眼无闪烁
    {Led_Proc, 1, 0}     // 1ms 快速响应逻辑
};

// 2. 初始化并运行
int main(void) {
    HAL_Init();
    SystemClock_Config();
    // 初始化调度器,传入数组和数量
    scheduler_init(myTasks, sizeof(myTasks) / sizeof(Task_t));
    while (1) {
        scheduler_run(); // 调度器开始巡逻
    }
}

进阶用法

如果需要支持只运行一次的任务,可以在 Task_t 结构体中增加一个 uint32_t runCount 成员。在 scheduler_run 中判断,每执行一次 runCount--,当减到 0 时不再执行。这能让你的调度器支持更复杂的业务逻辑。

技巧:我们可以约定,如果 runCount 设置为 0xFFFFFFFF(最大值),则代表该任务是永久循环执行;如果是其他数值,则代表剩余执行次数。

Task_t myTasks[] = {
    // 任务函数 周期 (ms) 初始时间 执行次数
    {Key_Proc, 20, 0, 0xFFFFFFFF}, // 永久循环执行
    {LCD_Proc, 200, 0, 0xFFFFFFFF}, // 永久循环执行
    {PowerOn_Msg, 1, 0, 1},         // 开机只执行 1 次
    {Beep_Alarm, 500, 0, 5}         // 报警响 5 次后自动停止
};

注意事项

虽然调度器实现了'伪并行',但它本质还是单线程。以下三点必须遵守:

  1. 严禁使用阻塞延时 在任何 Task_Proc 任务函数内部,绝对不能出现 HAL_Delay() 或长死循环。如果 LCD_Proc 阻塞了 50ms,那么 Key_Proc 就会在这 50ms 内完全失灵。
  2. 任务执行时间必须 < 任务周期 如果一个任务设定每 10ms 执行一次,但它本身逻辑运行需要 15ms,系统就会发生'任务追尾',导致其他任务的时间轴全部向后偏移。
  3. Systick 优先级 STM32 HAL 库默认 Systick 中断优先级最低(15)。如果你的应用中有极其耗时的中断处理函数,可能会导致 HAL_GetTick() 计时偏慢,建议提高优先级。

调试技巧

如果怀疑系统负载过高,可以在 scheduler_run 的循环开始前后翻转一个测试 IO 口。通过示波器观察该 IO 口的高电平时间,就能直观地看到所有任务总的执行耗时。

为了验证调度器的精准度并排查潜在的任务阻塞风险,我引入了 IO 电平翻转法 进行实测。在 LCD_Proc(屏幕刷新任务)的起始与结束位置分别插入 GPIO 操作代码,用于标记该任务的 CPU 占用区间:

void LCD_Proc(void) {
    // 在任务进入时拉高 PA7
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET);
    /* --- 屏幕刷新逻辑:sprintf、LCD 显示等 --- */
    // 在任务退出时拉低 PA7
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET);
}

逻辑分析仪实测结果

使用逻辑分析仪监听 PA7 引脚,捕获到的波形如下:

在这里插入图片描述

从测量图中可以看到,连续两个脉冲上升沿之间的时间间隔(Period)精确地维持在 200ms。这证明了我们的调度器在 while(1) 轮询下,依然能保持极高的定时精度。

注意:只要这个高电平时间 远小于 200ms(例如实测为 5ms),就说明系统负载尚有充足余量。如果高电平宽度接近甚至超过了任务周期(200ms),系统就会发生严重的'任务追尾',此时必须优化代码逻辑或拆分任务。

目录

  1. 开发环境
  2. 为什么选择 HAL 库实现?
  3. 核心原理
  4. 代码封装
  5. 使用案例
  6. 进阶用法
  7. 注意事项
  8. 调试技巧
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 本地部署运行大模型指南
  • Java 使用 Cursor 开发教程:IDEA 与 Cursor 双端开发及 Claude 3.7 模型接入
  • AWPortrait-Z LoRA 强度与推理步数协同调优指南
  • 宇树机器人g1二次开发:建图,定位,导航手把手教程(四)导航仿真部分:建完图之后打开仿真导航
  • LangChain 大模型应用开发框架详解
  • FPGA 与嵌入式开发对比:技术路线与职业前景分析
  • HarmonyOS Next DevEco Studio 端云一体化开发业务介绍
  • CS61B 课程笔记:图结构基础与深度优先遍历
  • Web-Check 部署与远程访问指南
  • C++ 内存布局、编译流程与关键字链接性
  • macOS 系统安装 OpenClaw 教程
  • SSH 免密登录配置详解
  • AI 行业周报:NVIDIA GTC 硬件路线图与各大模型动态解析
  • FPGA RGB 转 HDMI 显示系统原理与实现
  • 前端 Bug 排查实战:从现象定位到测试闭环的标准化流程
  • 前端 Network 性能优化场景解析
  • AI Agent 核心概念解析与 LangChain 实战指南
  • GLM-4.6V-Flash-WEB 实现高效结构化图像信息提取
  • DeepSeek 各版本演进与核心能力对比
  • Spring Boot 快速构建基于 Spring AI 的智能助手

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online