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 内核能用三句话讲清:
-
任务(task)= 一段独立运行的 C 函数 + 自己的栈。每个任务有 TCB(Task Control Block)记录优先级、状态、栈指针。任务在 ready / blocked / suspended 三态间切换,全部挂在
list.c的双向链表上。 -
调度 = 优先级位图 O(1) 选下一个任务。tick 中断(默认 1ms)一来,调度器查
uxTopReadyPriority位图找最高优先级的就绪任务,PendSV 异常做上下文切换汇编。同优先级任务走时间片轮转。 -
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.c 的 vTaskSwitchContext() 函数,加上 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)
踩过的坑
-
栈溢出乱跳:
configMINIMAL_STACK_SIZE默认值是按 demo 给的,自己业务里任务栈一旦溢出会直接踩到别的任务的 TCB,程序乱跳到奇怪地址。必须开configCHECK_FOR_STACK_OVERFLOW=2,调度时校验栈尾水印才能抓到。 -
heap 不是真正的内存安全方案:heap_4 / heap_5 用 first-fit + 合并空闲块,但 free 顺序错乱仍会产生碎片直到 malloc 返回 NULL。长寿命任务建议改用静态分配 API(
xTaskCreateStatic),把内存生命周期问题交给编译期。 -
ISR 里调错 API:中断里禁止调
vTaskDelay之类阻塞 API,必须用...FromISR后缀且把portYIELD_FROM_ISR放在中断退出前。新手最常见就是 ISR 直接xQueueSend触发 assert 重启。 -
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 多核支持,正式进入对称多处理时代
学到什么
- 三个文件 = 一个内核:list / queue / tasks 这三件事讲清,调度 + IPC + 内存就齐了
- queue 是统一原语:消息、信号量、互斥锁、事件组都是 queue 的特例,复用一份代码
- 优先级位图 + 双向链表 = O(1) 调度:这套数据结构不止 RTOS 在用,Linux O(1) 调度器思路一致
- portable 抽象层:上层代码完全平台无关,硬件差异全压在 portable/ 的汇编里——这是跨平台库的经典套路
延伸阅读
- 官方书:Mastering the FreeRTOS Real Time Kernel(Richard Barry 亲笔,免费 PDF)
- 视频:Shawn Hymel — Introduction to RTOS(Digi-Key 出品,14 集系列)
- 源码起步:FreeRTOS-Kernel GitHub 的
tasks.c看vTaskSwitchContext - csp-hoare-1978 —— FreeRTOS 的 queue 通信本质是 CSP 的工业精简版
- erlang-otp —— 同样基于消息传递,但目标是分布式而非 MCU
关联
- 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