Chapter 01 · FreeRTOS 实时操作系统

RTOS 与裸机开发

从裸机轮询与前后台系统的局限出发,理解 RTOS 与实时性的本质,认识 FreeRTOS 的移植结构与任务概念,写出第一个双任务程序。

课程进度8%

关键概念

裸机开发 (Bare-metal)
不使用任何操作系统,程序直接运行在硬件之上。典型结构是一个 while(1) 大循环轮询各任务,加上中断服务程序处理紧急事件。逻辑简单、资源开销最小,但任务耦合紧、实时性差、随功能增加会迅速变成难以维护的"意大利面代码"。
前后台系统 (Foreground/Background)
裸机开发的常见范式:主循环是"后台"(background),依次执行各功能模块;中断是"前台"(foreground),抢占后台处理实时事件。后台任务的响应时间取决于主循环一圈的最长执行时间,无法保证某个任务被及时处理。
RTOS(实时操作系统)
Real-Time Operating System,为嵌入式设备提供任务调度、任务间通信、时间管理和资源管理的内核。它把大循环拆成多个独立"任务",由调度器按优先级分配 CPU,使高优先级事件能在确定的时间内得到响应。FreeRTOS 是应用最广、免费开源(MIT 许可)的 RTOS 内核。
实时性:硬实时 vs 软实时
实时性强调"在确定时间内完成",而非"快"。硬实时:错过截止时间(deadline)等于系统失败,例如汽车气囊、电机换相,绝不允许超时。软实时:偶尔超时会降低服务质量但不致命,例如视频解码丢一帧、UI 刷新略卡。FreeRTOS 是一个确定性内核,可用于硬实时系统。
任务 (Task)
FreeRTOS 中最小的调度单位,本质是一个永不返回的 C 函数(内部通常是 for(;;) 循环)。每个任务拥有独立的栈空间、优先级和任务控制块 TCB。多个任务"看起来"在并发运行,实际是内核在单核 CPU 上快速切换它们的执行上下文。
调度器 (Scheduler)
FreeRTOS 内核的核心,负责决定"下一刻哪个任务运行"。它维护就绪任务列表,在系统节拍(Tick)中断或任务主动让出时,选择优先级最高的就绪任务投入运行。调度策略由 configUSE_PREEMPTION 等配置项决定。
上下文切换 (Context Switch)
调度器切换任务时,需要把当前任务的 CPU 寄存器(R0-R12、SP、LR、PC、xPSR 等)保存到它的栈里,再从下一个任务的栈中恢复寄存器。在 Cortex-M 上通过 PendSV 异常完成,这段汇编代码是 FreeRTOS 移植层(port.c)的核心。
移植层 (Port Layer)
FreeRTOS 与具体 CPU 架构相关的代码,位于 portable/ 目录,例如 port.cportmacro.h。它实现上下文切换、临界区进入/退出、SysTick 初始化等。用户只需选对内核架构(如 GCC/ARM_CM4F),无需改动通用内核 tasks.cqueue.c
FreeRTOSConfig.h
FreeRTOS 的裁剪配置文件,每个工程一份。通过一系列 config* 宏决定是否启用抢占、Tick 频率、最大优先级数、堆大小、是否包含互斥量/软件定时器等功能,从而在几 KB 的 MCU 上按需裁剪内核体积。

为什么裸机开发会遇到瓶颈

假设一个设备需要:每 10ms 采集一次 ADC、每 100ms 刷新 OLED、随时响应按键、每秒通过串口上报数据。用裸机的前后台方式写,通常长这样:

/* 裸机前后台系统:一个大循环轮询所有任务 */
int main(void) {
  HAL_Init();
  SystemClock_Config();
  BSP_Init();   /* 初始化 ADC/OLED/UART/按键 */

  while (1) {
    task_adc_sample();     /* 可能耗时 2ms  */
    task_oled_refresh();   /* 可能耗时 30ms —— 阻塞了整圈! */
    task_key_scan();       /* 按键要等 OLED 刷完才被扫到 */
    task_uart_report();    /* 串口发送也要排队等待 */
  }
}
前后台系统的三大痛点

① 响应延迟不可控:OLED 刷新耗时 30ms 时,按键要等 30ms 后才被扫描到,无法保证响应时间上限。② 任务耦合:每个函数都要写成"非阻塞"的状态机形式,不能用简单的 HAL_Delay(),否则整圈卡死。③ 难以扩展:新增功能要小心插入循环,一处耗时增加就影响全局,维护成本随功能数量非线性上升。

RTOS 如何解决:把大循环拆成独立任务

用 FreeRTOS,上面四个功能变成四个独立任务,各自可以放心地"阻塞式"延时。内核保证高优先级任务能及时抢占低优先级任务:

裸机 vs RTOS 执行模型对比 ═══════════════════════════════════════════════════════════════ 裸机(前后台):单条时间线,串行轮询 ┌────────────────────────────────────────────────────────┐ │ ADC → OLED(30ms) → KEY → UART → ADC → OLED(30ms) → ... │ └────────────────────────────────────────────────────────┘ 按键在此按下 ↑ 实际响应 ↑ (延迟 ~30ms 不可控) RTOS(抢占式):调度器按优先级分配 CPU ┌──────────────────────────────────────────────────────────┐ │ KeyTask (prio 3) ████ ██ (随时抢占) │ │ AdcTask (prio 2) ██ ██ ██ ██ │ │ OledTask(prio 1) ████████ ████████ │ │ IdleTask(prio 0) ▒▒ ▒▒ ▒▒ (CPU 空闲) │ └──────────────────────────────────────────────────────────┘ t=0 t=1ms t=2ms ... 时间片由 Tick 驱动

FreeRTOS 的源码结构与移植

FreeRTOS 内核非常精简,核心只有几个 C 文件。理解目录结构,才能正确地把它加入你的工程:

FreeRTOS 源码目录结构 FreeRTOS/Source/ ├── tasks.c // 任务创建/调度/状态管理(内核核心) ├── queue.c // 队列、信号量、互斥量的底层实现 ├── list.c // 内核用的双向链表(就绪/延时列表) ├── timers.c // 软件定时器 ├── event_groups.c // 事件组 ├── stream_buffer.c // 流缓冲 / 消息缓冲 ├── include/ // FreeRTOS.h、task.h、queue.h ... 通用头文件 └── portable/ ├── GCC/ARM_CM4F/ // port.c + portmacro.h(Cortex-M4F 移植层) ├── RVDS/ARM_CM4F/ // Keil 版本的移植层 └── MemMang/ // heap_1.c ~ heap_5.c 内存分配方案 你的工程/ └── FreeRTOSConfig.h // 裁剪配置(放在你的工程 Inc 目录)
CubeMX 一键集成

用 STM32CubeMX 可在 Middleware → FREERTOS 里勾选 CMSIS_V2 接口,自动把上述文件加入工程并生成 FreeRTOSConfig.h 与默认任务。ESP-IDF 则天生内置 FreeRTOS,直接 #include "freertos/FreeRTOS.h" 即可使用(注意 ESP32 是双核 SMP 版本)。本教程以标准 Cortex-M 单核 FreeRTOS 为主讲解,代码在 ESP32 上同样适用。

FreeRTOSConfig.h 关键配置

下面是一份最小可用的配置片段,每一项都直接影响内核行为:

/* FreeRTOSConfig.h — 核心配置项 */
#define configUSE_PREEMPTION            1   /* 1=抢占式  0=协作式 */
#define configCPU_CLOCK_HZ              168000000 /* CPU 主频 */
#define configTICK_RATE_HZ             1000 /* 系统节拍 1000Hz → 1ms 一个 tick */
#define configMAX_PRIORITIES            8   /* 优先级数:0(最低)~7(最高) */
#define configMINIMAL_STACK_SIZE       128 /* 空闲任务栈:单位是字(word)=4字节 */
#define configTOTAL_HEAP_SIZE          (15*1024) /* 内核堆总大小 15KB */
#define configUSE_16_BIT_TICKS          0   /* 32 位 tick 计数(Cortex-M 用 32 位) */

#define configUSE_MUTEXES               1   /* 启用互斥量 */
#define configUSE_TIMERS                1   /* 启用软件定时器 */
#define configUSE_IDLE_HOOK             0   /* 是否启用空闲钩子 */
#define configCHECK_FOR_STACK_OVERFLOW  2   /* 栈溢出检测方法 2 */

/* Cortex-M 中断优先级配置(第 9 章详解) */
#define configPRIO_BITS                 4
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY        15
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY  5

第一个程序:双任务闪灯 + 打印

下面创建两个任务:LedTask 每 500ms 翻转一次 LED,PrintTask 每 1s 打印一次心跳。两者独立并发,各自用 vTaskDelay 放心阻塞——这正是 RTOS 相较裸机最直观的好处。

#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include <stdio.h>

/* 任务句柄,用于后续管理任务 */
TaskHandle_t g_ledHandle   = NULL;
TaskHandle_t g_printHandle = NULL;

/* 任务函数原型:参数固定为 void*,函数永不返回 */
void LedTask(void *pvParameters) {
  (void) pvParameters;                  /* 未用到参数,避免告警 */
  for (;;) {                             /* 任务主体是无限循环 */
    HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
    vTaskDelay(pdMS_TO_TICKS(500));  /* 阻塞 500ms,让出 CPU */
  }
}

void PrintTask(void *pvParameters) {
  (void) pvParameters;
  uint32_t beat = 0;
  for (;;) {
    printf("heartbeat #%lu, free heap = %u\r\n",
           beat++, (unsigned)xPortGetFreeHeapSize());
    vTaskDelay(pdMS_TO_TICKS(1000));
  }
}

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

  /* 创建任务:函数、名字、栈深(word)、参数、优先级、句柄 */
  xTaskCreate(LedTask,   "LED",   128, NULL, 1, &g_ledHandle);
  xTaskCreate(PrintTask, "PRINT", 256, NULL, 2, &g_printHandle);

  /* 启动调度器:此函数正常情况下永不返回 */
  vTaskStartScheduler();

  /* 只有堆不足以创建空闲任务时才会执行到这里 */
  while (1) { }
}
任务函数绝不能 return

任务函数必须是无限循环 for(;;)。如果某个任务的函数体执行完毕并 return,会导致栈失衡、程序跑飞或触发 configASSERT。若确实需要"用完即弃"的任务,应在函数末尾调用 vTaskDelete(NULL) 让内核回收自己,而不是直接返回。

xTaskCreate 的栈深度单位陷阱

参数含义常见误区
pxTaskCode任务函数指针
pcName任务名(调试用字符串)长度受 configMAX_TASK_NAME_LEN 限制
usStackDepth栈深度,单位是字(word)Cortex-M 上 1 word=4 字节,填 128 实际是 512 字节,别当成字节
pvParameters传给任务的参数不要传栈上局部变量地址
uxPriority优先级,数值越大越高不得 ≥ configMAX_PRIORITIES
pxCreatedTask返回任务句柄不需要可传 NULL
本章小结

裸机开发用 while(1) 大循环轮询任务,构成前后台系统:主循环是后台、中断是前台。其致命缺陷是响应延迟不可控、任务强耦合、难以扩展。RTOS 把大循环拆成多个独立任务,由调度器按优先级分配 CPU,让高优先级事件在确定时间内被响应——这就是实时性(硬实时=错过截止即失败,软实时=偶尔超时可接受)。FreeRTOS 是免费开源、应用最广的确定性内核,源码核心是 tasks.c/queue.c/list.c,CPU 相关代码在 portable/ 移植层,用户只需维护 FreeRTOSConfig.h 裁剪配置。任务是最小调度单位,本质是永不返回的 for(;;) 函数,拥有独立栈与优先级;切换任务时通过 PendSV 完成上下文切换。用 xTaskCreate() 创建任务(注意栈深单位是字,非字节),最后 vTaskStartScheduler() 启动调度器且永不返回。任务函数绝不能 return,用完应 vTaskDelete(NULL)