跳转到内容

FreeRTOS-Kernel — KB 级 RAM 跑得动的可抢占多任务内核

是什么

FreeRTOS 是一个用 C 写的 实时操作系统内核(RTOS = Real-Time Operating System),专门跑在 KB 级 RAM、MHz 级主频的 MCU(微控制器)上。

日常类比:Linux 像一个城市的交通系统,红绿灯多、规则复杂、车随时可能堵;FreeRTOS 像消防车专用通道——任务来了必须在毫秒内响应,谁优先级高谁先走,绝不堵车。

核心规模极小:

  • 只用 list.c + queue.c + tasks.c 三个文件,就能跑起完整的可抢占多任务内核
  • 全部源码 ~10k 行 C,能塞进 4KB ROM、不到 1KB RAM 的 Cortex-M0
  • 2003 年由 Richard Barry 开源,2017 年 AWS 接管并把协议从 GPL 切到 MIT

它是嵌入式领域部署量最大的 RTOS,是阅读 ~10k 行 C 学 调度 + IPC + 内存 全栈的标准教科书。

为什么重要

不理解 FreeRTOS,下面这些事都搞不清:

  • 为什么你家空调遥控器、智能手环、Wi-Fi 路由器里跑的不是 Linux 而是 RTOS——Linux 内核动辄几 MB,MCU 装不下
  • 为什么 ESP32 / STM32 这些主流 MCU 默认带 FreeRTOS 移植——它已是事实标准
  • 为什么 ~10k 行 C 就能实现 “硬实时调度 + 任务间通信 + 动态内存”——RTOS 的精简到底精简了什么
  • 工业控制、医疗设备、汽车 ECU 里那个跑了二十年没崩过的小固件,多半就是 FreeRTOS 的衍生版(SafeRTOS,IEC 61508 SIL3 认证)

核心要点

FreeRTOS 内核能用三句话讲清:

  1. 任务(task)= 一段独立运行的 C 函数 + 自己的栈。每个任务有 TCB(Task Control Block)记录优先级、状态、栈指针。任务在 ready / blocked / suspended 三态间切换,全部挂在 list.c 的双向链表上。

  2. 调度 = 优先级位图 O(1) 选下一个任务。tick 中断(默认 1ms)一来,调度器查 uxTopReadyPriority 位图找最高优先级的就绪任务,PendSV 异常做上下文切换汇编。同优先级任务走时间片轮转。

  3. IPC(任务间通信)= queue 一招打天下xQueueSend / xQueueReceive 既是消息队列,也是信号量(计数 = 1 的 queue)、互斥锁(带优先级继承的 queue)、事件组(位图 queue)的底层。一份代码服务四种原语。

加上 portable/ 目录里架构相关的汇编(PendSV、SVC、SysTick),覆盖 ARM Cortex-M0/M3/M4/M7、RISC-V、Xtensa 等数十种 MCU。

实践案例

案例 1:三任务协作 + queue 传数据

QueueHandle_t xSensorQueue;
void vSensorTask(void *pv) {
int sample;
for (;;) {
sample = read_adc();
xQueueSendToBack(xSensorQueue, &sample, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(10)); // 10ms 一次
}
}
void vProcessTask(void *pv) {
int sample;
for (;;) {
xQueueReceive(xSensorQueue, &sample, portMAX_DELAY);
process(sample);
}
}
xSensorQueue = xQueueCreate(16, sizeof(int));
xTaskCreate(vSensorTask, "sensor", 256, NULL, 3, NULL);
xTaskCreate(vProcessTask, "process", 512, NULL, 2, NULL);
vTaskStartScheduler();

vSensorTask 优先级高,每 10ms 采一次;vProcessTask 在 queue 上阻塞,有数据就处理。两者通过 queue 解耦,时序由内核负责。

案例 2:中断 → 任务的优雅升级

GPIO 中断里不能直接跑业务(中断必须短)。FreeRTOS 的标准做法是 ISR 里 xSemaphoreGiveFromISR 放一个二值信号量,任务在信号量上 take,把响应从 “中断里裸算” 升级成 “任务里慢慢算”——比裸轮询响应快 10-100 倍,还不阻塞别的中断。

案例 3:读 50 行核心调度代码

tasks.cvTaskSwitchContext() 函数,加上 taskSELECT_HIGHEST_PRIORITY_TASK() 宏,总共 50 行 C 就讲清楚了 “优先级位图 + 双向链表 = O(1) 调度”:

  • uxTopReadyPriority 是个 32 位位图,每位代表一个优先级是否有就绪任务
  • 找最高优先级用 __CLZ(Count Leading Zeros)汇编指令,1 个时钟周期搞定
  • 找到优先级后,从对应的双向链表头取出任务,链表头指针往后挪一位(实现轮转)

读完这 50 行,就理解了所有 RTOS 调度器的本质——也包括 Linux 2.6 的 O(1) 调度器,思路完全一致。

案例 4:heap_1~heap_5 五种内存方案

FreeRTOS 给了 5 种内存方案,按硬件特点选:

  • heap_1:只 malloc 不 free,用于跑完一次永不退出的任务
  • heap_2:first-fit 但不合并空闲块,碎片化快但简单
  • heap_3:直接包装 libc 的 malloc/free,前提是 libc 线程安全
  • heap_4:first-fit + 合并相邻空闲块(最常用)
  • heap_5:heap_4 基础上支持多个不连续的内存区段(适合外挂 SDRAM)

踩过的坑

  1. 栈溢出乱跳configMINIMAL_STACK_SIZE 默认值是按 demo 给的,自己业务里任务栈一旦溢出会直接踩到别的任务的 TCB,程序乱跳到奇怪地址。必须开 configCHECK_FOR_STACK_OVERFLOW=2,调度时校验栈尾水印才能抓到。

  2. heap 不是真正的内存安全方案:heap_4 / heap_5 用 first-fit + 合并空闲块,但 free 顺序错乱仍会产生碎片直到 malloc 返回 NULL。长寿命任务建议改用静态分配 API(xTaskCreateStatic),把内存生命周期问题交给编译期。

  3. ISR 里调错 API:中断里禁止调 vTaskDelay 之类阻塞 API,必须用 ...FromISR 后缀且把 portYIELD_FROM_ISR 放在中断退出前。新手最常见就是 ISR 直接 xQueueSend 触发 assert 重启。

  4. tickless idle 的时间漂移:开 configUSE_TICKLESS_IDLE 低功耗模式时,xTaskGetTickCount 不再按 1ms 精度走,所有基于 tick 的 timeout 会被低功耗时间补偿。不理解就会以为定时器漂移,其实是省电换来的。

适用 vs 不适用场景

适用

  • 资源受限的 MCU(KB 级 RAM、MHz 级主频)需要多任务
  • 硬实时要求(中断到任务响应 < 100μs)
  • 任务数 < 50、栈大小可控的固件
  • 教学:读 ~10k 行 C 学 RTOS 全栈

不适用

  • 需要 MMU / 进程隔离 → 用 Linux 或 Zephyr
  • 需要文件系统 / 网络协议栈 / GUI 一体化 → 用 RT-Thread / ThreadX 等带组件的
  • 需要 SMP(多核)调度——FreeRTOS 11.0 才加 SMP 支持,生态还不成熟
  • 安全认证场景(航空、医疗)→ 用商用衍生版 SafeRTOS

历史小故事(可跳过)

  • 2003 年:Richard Barry 个人项目开源 FreeRTOS,目标 “让每个 MCU 工程师都用得起 RTOS”
  • 2009 年:Wittenstein High Integrity Systems 基于 FreeRTOS 做出 SafeRTOS,拿下 IEC 61508 SIL3 认证
  • 2017 年:AWS 收购 FreeRTOS,协议从修改版 GPL 切到 MIT,Richard Barry 加入 AWS 继续维护
  • 2024 年:FreeRTOS 11.0 发布 SMP 多核支持,正式进入对称多处理时代

学到什么

  1. 三个文件 = 一个内核:list / queue / tasks 这三件事讲清,调度 + IPC + 内存就齐了
  2. queue 是统一原语:消息、信号量、互斥锁、事件组都是 queue 的特例,复用一份代码
  3. 优先级位图 + 双向链表 = O(1) 调度:这套数据结构不止 RTOS 在用,Linux O(1) 调度器思路一致
  4. portable 抽象层:上层代码完全平台无关,硬件差异全压在 portable/ 的汇编里——这是跨平台库的经典套路

延伸阅读

关联

  • csp-hoare-1978 —— Hoare CSP:queue 通信的理论原型
  • hewitt-actor-model —— Actor 模型:另一种 “任务 + 消息” 抽象,但运行时差异巨大
  • erlang-otp —— Erlang/OTP:消息传递并发的另一极(数百万轻量进程)
  • dijkstra-shortest-path —— Dijkstra 信号量原始论文(FreeRTOS 信号量的祖宗)
  • ebpf —— eBPF:另一种 “在受限环境跑用户代码” 的精简内核思路

反向链接

  • buildroot —— Buildroot — 用 Make 给嵌入式板子烤一张完整 Linux 镜像
  • csp-hoare-1978 —— CSP — 进程之间只许喊话不许共用内存
  • dijkstra-shortest-path —— Dijkstra 最短路径 — 一杯咖啡时间想出来的贪心算法
  • ebpf —— eBPF — 用户写小程序,内核证明安全后再跑
  • embassy —— Embassy — 嵌入式 Rust 的 async/await 运行时
  • embedded-hal —— embedded-hal — 让同一份驱动代码跑在任意芯片上
  • erlang-otp —— Erlang OTP — 容错并发系统设计
  • freemodbus —— FreeModbus — 嵌入式 Modbus RTU/TCP 从机协议栈
  • hewitt-actor-model —— Hewitt Actor 模型 — 把计算拆成一群只会发消息的小邮筒
  • lwip —— lwIP — ~40KB ROM 跑完整 TCP/IP 的嵌入式网络栈
  • mbedtls —— Mbed TLS — 嵌入式设备的 TLS 1.3 / X.509 / 加密原语库
  • nuttx —— Apache NuttX — POSIX 接近完整的小型实时操作系统
  • openthread —— OpenThread — Google 开源的 Thread mesh 网络协议栈
  • platformio-core —— PlatformIO Core — 一套命令行,统管千块嵌入式开发板
  • probe-rs —— probe-rs — Rust 写的嵌入式烧录与调试工具
  • smoltcp —— smoltcp — 不依赖操作系统的 Rust TCP/IP 协议栈
  • zephyr —— Zephyr — 一份代码树跑遍所有嵌入式芯片的开源 RTOS