1. 课程导论与开发环境搭建

1.1 为什么需要任务调度?

先问大家一个问题:你写单片机程序时,是不是经常这样——

  • 主循环里放一堆函数,一个一个轮着跑
  • 用延时死等,CPU 在那空转
  • 中断一多,逻辑就乱成一锅粥

我以前刚做嵌入式那会儿,也这么干。直到有一次,一个温控项目出了问题:按键扫描、LCD 刷新、温度采集、PID 计算全塞在主循环里。结果按键按下去,要等半秒才有反应。用户投诉说「这机器是不是死机了?」

嗯,问题就出在——没有任务调度。

任务调度,说白了就是让 CPU 知道:什么时候该干什么事。比如:

  • 每 1ms 扫描一次按键
  • 每 10ms 刷新一次屏幕
  • 每 100ms 采集一次温度
  • 每 500ms 上报一次数据

如果没有调度,你就得自己算时间、自己拆延时、自己处理优先级。项目一复杂,代码就变成「面条代码」——牵一发而动全身。

核心观点:任务调度不是 RTOS 的专利。裸机一样可以做调度,而且很多时候,裸机调度比 RTOS 更轻量、更可控。

1.2 裸机 vs RTOS:怎么选?

很多新手会纠结:到底用裸机还是上 RTOS?

我个人的经验是——看项目复杂度。别盲目上 RTOS,也别死磕裸机。

对比维度 裸机调度 RTOS
资源占用 极低,几 KB ROM 即可 通常需要 10KB+ ROM
实时性 可控,但需手动优化 内核保证,优先级明确
任务数量 适合 3~8 个任务 适合 10+ 个任务
调试难度 简单,直接看代码流 需要理解内核机制
学习曲线 平缓 较陡
典型场景 传感器采集、简单控制 多协议、多外设、复杂状态机

你看,没有绝对的好坏。我做过一个智能家居网关,8 个任务,裸机调度跑得稳稳的。也做过一个工业控制器,20 多个任务,不上 RTOS 根本管不过来。

我的建议:如果你的项目任务数 ≤ 5,且实时性要求不高,裸机调度完全够用。如果任务数 ≥ 10,或者有严格的 deadline 要求,考虑 RTOS。

那这门课讲什么?——裸机任务调度。我会带你从零搭建一套轻量级调度框架,配合中断管理,让你在裸机上也能实现「类 RTOS」的效果。

1.3 课程知识体系

先看看我们这 30 章要学什么。我画了一张图,帮你建立全局视角:

STM32裸机任务调度与中断管理 知识体系 基础篇(第1-5章) 环境搭建 · 时钟 · GPIO · 定时器 调度篇(第6-15章) 时间片 · 优先级 · 状态机调度 中断篇(第16-22章) NVIC · 抢占 · 临界区 · 嵌套 实战篇(第23-28章) 多传感器采集 · 按键消抖 · 通信协议调度 · 低功耗调度 进阶篇(第29-30章) 性能分析 · 调试技巧 · 裸机→RTOS迁移 📌 课程设计思路 • 从基础外设驱动开始,逐步构建调度内核 • 每个调度策略都配有完整工程示例 • 中断管理贯穿始终,与调度深度耦合 • 最终目标是:裸机写出「类RTOS」的工程代码

你看,整个课程是层层递进的。先打好基础,再学调度和中断,然后实战演练,最后进阶提升。别跳着看——我踩过的坑,不想让你再踩一遍。

1.4 开发环境搭建

好,理论说完了,咱们动手。工欲善其事,必先利其器。

1.4.1 安装 STM32CubeIDE

我推荐用 STM32CubeIDE,原因有三:

  1. 官方出品,兼容性最好
  2. 免费,没有 license 限制
  3. 集成 CubeMX,图形化配置外设

安装步骤很简单:

  • 去 ST 官网下载最新版 STM32CubeIDE
  • 双击安装,一路 Next
  • 选择安装路径(建议不要有中文)
  • 等待安装完成

注意:安装路径不要有空格和中文。我以前吃过这个亏——项目路径带中文,编译报错查了半天,结果是路径问题。

1.4.2 创建工程模板

我们创建一个基础工程模板,后续所有章节都基于这个模板。

步骤:

  1. 打开 STM32CubeIDE,点击 File → New → STM32 Project
  2. 选择芯片型号(我用的 STM32F103C8T6,你随意)
  3. 在 Pinout & Configuration 中配置:
    • RCC → HSE → Crystal/Ceramic Resonator
    • SYS → Debug → Serial Wire
    • 时钟树 → HCLK 设为 72MHz
  4. Project Manager 设置:
    • Project Name: stm32_scheduler_template
    • Toolchain / IDE: STM32CubeIDE
  5. 点击 Finish,生成工程

生成后,我们添加一个基础调度框架文件。先创建两个文件:

Core/
├── Inc/
│   └── scheduler.h      // 调度器头文件
└── Src/
    └── scheduler.c      // 调度器源文件

这是最简调度器的头文件骨架:

// scheduler.h
#ifndef __SCHEDULER_H
#define __SCHEDULER_H

#include "main.h"

/* 任务控制块 */
typedef struct {
    void (*task_func)(void);   // 任务函数指针
    uint32_t period_ticks;     // 任务周期(tick数)
    uint32_t elapsed_ticks;    // 已运行tick数
    uint8_t  running;          // 运行标志
} Task_t;

/* 调度器初始化 */
void Scheduler_Init(void);

/* 注册任务 */
uint8_t Scheduler_RegisterTask(void (*func)(void), uint32_t period_ms);

/* 调度器运行(放在主循环) */
void Scheduler_Dispatch(void);

/* 系统tick中断处理(放在SysTick_Handler) */
void Scheduler_TickHandler(void);

#endif

这个框架很简单,但它是我们后面所有调度策略的基础。现在先不展开实现细节——后面章节会一步步完善。

小技巧:每次新建工程,我都用这个模板。省去重复配置的时间,直接聚焦业务逻辑。

1.5 验证环境

写一个简单的测试代码,验证环境是否正常:

// main.c
#include "main.h"
#include "scheduler.h"

void Task1_LED(void) {
    HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}

void Task2_UART(void) {
    printf("Scheduler running...\r\n");
}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();

    Scheduler_Init();

    Scheduler_RegisterTask(Task1_LED, 500);   // 500ms 翻转LED
    Scheduler_RegisterTask(Task2_UART, 1000); // 1000ms 打印信息

    while (1) {
        Scheduler_Dispatch();
    }
}

编译下载,如果 LED 每 500ms 闪烁一次,串口每 1s 打印一次——恭喜,环境搭建成功!

记住:这个模板就是你的「嵌入式瑞士军刀」。后面所有章节的代码,都基于这个框架扩展。

好,第一章就到这里。环境搭好了,框架有了,接下来我们就要深入调度器的核心——时间片轮转、优先级调度、状态机调度……一步步把裸机调度玩出花来。