1. 课程导论与项目全景:为什么需要任务调度?为什么需要硬件抽象层?

各位同学好,我是你们这门课的老王。在嵌入式这行摸爬滚打了十几年,见过太多项目从原型到量产的血泪史。今天咱们开篇第一讲,不急着敲代码,先聊聊两个最根本的问题:任务调度和硬件抽象层,到底为什么非用不可?

说白了,这两个东西就是嵌入式软件的骨架和皮肤。骨架撑得住,系统才不乱;皮肤换得勤,硬件才不绑死你。我见过不少团队,前期图省事,在主循环里堆逻辑,结果后期加功能加得想哭。嗯,咱们今天就把这些坑提前填上。

1.1 裸机开发的痛点:你迟早会遇到的麻烦

先看一个典型场景。你写了一个温控器,主循环大概长这样:

void main(void)
{
    while(1)
    {
        read_temperature();   // 读传感器,耗时 5ms
        process_pid();        // PID 计算,耗时 3ms
        update_display();     // 刷新屏幕,耗时 20ms
        check_keypad();       // 扫描按键,耗时 2ms
        delay_ms(10);         // 凑个周期
    }
}

看起来挺顺?问题大了去了。

  • 实时性差:按键扫描要等显示刷新完才能执行。用户按了键,得等 20ms 才有反应。你想想看,这要是工业急停按钮,20ms 可能就出事故了。
  • 耦合严重:所有功能挤在一个循环里。想加个蓝牙通信?得重新排时间片。想换个大屏?显示函数改了,整个循环节奏都变。
  • 维护噩梦:三个月后回看代码,你自己都分不清哪个延时是给传感器稳定的,哪个是凑周期的。
我曾经踩过的坑: 2016 年做一个多路数据采集器,裸机循环里塞了 8 个任务。测试时一切正常,量产 500 台后,客户反馈偶尔丢数据。查了三天,发现是某次按键长按导致传感器读取被延迟,采样窗口错过了。从那以后,我再也不敢在正式产品里用纯裸机大循环。

1.2 任务调度:让 MCU 学会「多线程」思考

任务调度解决的核心问题就一个:谁该什么时候跑。

MCU 是单核的,同一时刻只能干一件事。但我们可以通过快速切换,让每个任务都觉得「CPU 是我的」。这就是调度的本质——分时复用。

常见的调度策略有这几种:

调度方式 原理 适用场景 我个人的评价
前后台(超级循环) 中断处理紧急事件,主循环轮询 极简系统,任务少于 5 个 入门可以,产品慎用
时间片轮转 每个任务分配固定时间片 任务周期相对固定 简单可靠,我常用
优先级抢占 高优先级任务打断低优先级 实时性要求高的场景 功能强,但要注意优先级反转
协作式调度 任务主动让出 CPU 资源极度受限的 MCU 省 RAM,但怕任务死等

咱们这门课,会从最简单的协作式开始,一步步搭建一个可抢占的调度内核。别怕,代码量不大,但思路一定要理清。

1.3 硬件抽象层:给代码穿上「防弹衣」

硬件抽象层,英文叫 HAL(Hardware Abstraction Layer)。说白了,就是在你的应用代码和芯片寄存器之间,加一层「翻译官」。

为什么要加这一层?我直接操作寄存器不香吗?

香,但只香一次。举个例子:

// 直接操作寄存器——STM32 的 GPIO
GPIOA->ODR |= (1 << 5);   // PA5 输出高

// 换到 GD32,寄存器地址变了
GPIOA->BSRR = (1 << 5);   // 语法都不一样

你看,换个芯片,所有 GPIO 操作都得重写。这还只是一个引脚。要是项目里有 SPI、I2C、UART、定时器……那工程量,想想都头大。

有了 HAL 层,你的应用代码只跟 HAL 接口打交道:

// HAL 层接口——与芯片无关
void hal_gpio_write(uint8_t pin, uint8_t level);
uint8_t hal_gpio_read(uint8_t pin);
void hal_uart_send(uint8_t *data, uint16_t len);

换芯片?只需要重写 HAL 层的实现。应用层代码,一行不动。

我的习惯: 做项目前,先花半天把 HAL 接口定义好。哪怕初期只用到 3 个外设,也把接口架子搭全。后面加功能时,你会感谢当初的自己。

1.4 调度器 + HAL:1+1 > 2 的组合

单独用调度器,或者单独用 HAL,都能解决问题。但把它们组合起来,才是真正的「解耦大法」。

来看这张架构图,这就是咱们整个课程的核心骨架:

MCU 软件分层架构图 MCU 软件分层架构(课程核心骨架) 应用层(Application) 温控逻辑 · 数据采集 · 通信协议 · 人机交互 任务调度层(Scheduler) 任务创建 · 优先级管理 · 时间片分配 · 上下文切换 硬件抽象层(HAL) GPIO · UART · SPI · I2C · Timer · ADC 接口 MCU 硬件层(Hardware) 寄存器 · 中断控制器 · 外设模块 · 时钟树 解耦收益 应用层 不关心硬件 调度层 不关心外设 HAL 层 不关心业务 硬件层 只暴露寄存器 换芯片 只改 HAL 实现 加功能 只加应用任务

这张图我希望你刻在脑子里。从上到下,每一层只依赖它的下一层。应用层不知道 GPIO 寄存器在哪,调度层不知道传感器是 I2C 还是 SPI。这就是解耦的精髓。

1.5 课程目标:30 章后你能带走什么

咱们这门课不是纯理论,也不是纯代码。我设计的目标很务实:

  • 手写一个轻量级任务调度器:支持协作式和抢占式两种模式,代码量控制在 500 行以内,适合任何 Cortex-M 内核 MCU。
  • 搭建一套可移植的 HAL 接口:覆盖 GPIO、UART、SPI、I2C、Timer 五大外设,接口风格统一,换芯片只需改底层实现。
  • 完成一个综合实战项目:多传感器数据采集 + 显示 + 通信,全部跑在你自己的调度器上。
  • 掌握调试与性能分析方法:任务超时怎么查?CPU 占用率怎么算?这些实战技巧我都会教。
一句话总结: 学完这门课,你拿到任何一款 MCU,都能在半天内搭好调度框架和 HAL 层,然后专心写业务逻辑。这才是嵌入式工程师该有的效率。

1.6 最终项目演示:一个真实的调度系统长什么样

咱们最终要做的项目,是一个「智能环境监测站」。它同时跑着 6 个任务:

  1. 温度采集任务(周期 500ms,优先级中)
  2. 湿度采集任务(周期 500ms,优先级中)
  3. 气压采集任务(周期 1000ms,优先级低)
  4. OLED 显示任务(周期 200ms,优先级中高)
  5. 按键扫描任务(周期 20ms,优先级高)
  6. 串口通信任务(事件触发,优先级中)

所有任务都在咱们自己写的调度器上运行。HAL 层封装了 STM32 的底层操作。如果你想移植到 GD32 或者 AT32,只需要改 HAL 层的 5 个文件,应用代码一行不动。

嗯,这就是解耦的力量。也是咱们接下来 29 章要一起完成的事。