1. 为什么第一个项目就选 FreeRTOS
1.1 从裸机到实时操作系统的分水岭
很多做单片机的朋友都有类似的经历:刚开始用 STM32 或者 GD32 写裸机程序,一个while(1)大循环加上几个中断,项目就跑起来了。点个灯、读个串口、驱动个 OLED,这些都不在话下。但当项目稍微复杂一点,比如要同时处理按键、串口协议解析、传感器采集、屏幕刷新、低功耗管理,裸机那套delay加状态机就开始捉襟见肘了。你会发现代码里到处是HAL_Delay,一个地方卡住整个系统就僵住了,响应时间完全没法保证。
这时候 FreeRTOS 就登场了。它本质上是一个实时内核,把 CPU 的时间切片分配给不同的任务,让每个任务看起来都在"同时"运行。和 Linux 这种通用操作系统不同,FreeRTOS 的调度器非常小巧,内核编译出来通常只有几 KB 到十几 KB,RAM 占用也可以压到几 KB,跑在 Cortex-M3、M4 这类资源有限的 MCU 上毫无压力。它解决的核心问题就是:让多个功能模块互不阻塞地并发运行,并且保证高优先级任务的响应时间可预测。
这个系列我打算从零开始,把 FreeRTOS 从环境搭建到内核机制、再到项目实战完整走一遍。今天是第 1 天,目标很明确:把开发环境搭起来,让第一个 FreeRTOS 任务在板子上跑起来。适合谁看?有 C 语言基础、用过 STM32 或者类似 MCU、想从裸机进阶到 RTOS 的嵌入式开发者。如果你连 GPIO 都没点过灯,建议先把裸机基础补一补再回来。
1.2 环境搭建这件事,坑比想象中多
别看"环境搭建"四个字简单,实际动手的时候问题一大堆:Keil 版本和器件包不匹配、CubeMX 生成的代码编译报错、FreeRTOS 源码版本和内核接口对不上、下载器识别不到芯片、串口打印乱码……我见过太多人卡在环境这一步就放弃了。所以这篇会把每个环节的坑都提前说清楚,让你少走弯路。
环境搭建的核心思路是:用 STM32CubeMX 做图形化配置,自动生成包含 FreeRTOS 的工程骨架,再用 Keil MDK 或者 STM32CubeIDE 编译下载。这条路线对新手最友好,因为 CubeMX 帮你处理了时钟树、引脚复用、中断优先级这些容易出错的地方,FreeRTOS 的移植代码也是官方维护的,稳定性有保障。当然,如果你想深入理解移植过程,后面我也会讲手动移植的方法,但第一天我们先跑起来再说。
2. 开发环境整体设计与工具选型
2.1 硬件平台怎么选
硬件这块,我建议直接用STM32F103C8T6 最小系统板,也就是大家常说的"蓝板"或者"核心板"。理由很实在:价格便宜,十几块钱就能买到;资料铺天盖地,遇到问题一搜就有答案;Cortex-M3 内核,FreeRTOS 官方支持非常成熟;64KB Flash + 20KB RAM,跑几个任务绰绰有余。如果你手头有 STM32F407 探索板或者 Nucleo 板子也完全可以,资源更充裕,后面做复杂项目更从容。
需要准备的硬件清单:
- STM32 开发板一块(F103C8T6 或 F407 均可)
- ST-Link V2 下载器一个(或者板载 DAP-Link)
- USB 转串口模块一个(CH340 就行,用来看串口打印)
- 杜邦线若干
- 面包板一块(可选,方便接线)
注意:市面上有些便宜的 ST-Link 克隆版驱动有问题,Windows 11 下可能识别不出来。如果遇到这种情况,去 ST 官网下载最新的 ST-Link 驱动,或者换一个 DAP-Link 下载器,后者免驱即插即用,省心很多。
2.2 软件工具链的取舍
软件方面有三条主流路线,我做个对比:
| 工具组合 | 优点 | 缺点 | 适合人群 |
|---|---|---|---|
| CubeMX + Keil MDK | 生态成熟,编译快,调试方便 | Keil 收费,社区版有代码限制 | 企业开发、老手 |
| CubeMX + STM32CubeIDE | 完全免费,基于 Eclipse | 界面略卡,编译速度一般 | 学生、个人开发者 |
| CubeMX + VSCode + GCC | 免费,编辑器强大 | 配置复杂,调试需额外折腾 | 喜欢折腾的极客 |
我个人推荐CubeMX + Keil MDK这套组合,因为 Keil 的调试体验确实好,断点、变量监视、外设寄存器查看都很顺手。Keil 有 32KB 代码限制的评估版,但 FreeRTOS 的入门工程编译出来通常不超过这个限制,学习完全够用。如果你不想碰版权问题,STM32CubeIDE 是零成本的最佳选择,功能上不比 Keil 差多少。
软件版本建议:
- STM32CubeMX:6.10 或更高版本
- Keil MDK:5.38 或更高(记得装 ARM Compiler 6)
- STM32CubeIDE:1.14 或更高
- ST-Link 驱动:最新版
- 串口助手:XCOM、SSCOM 或者 MobaXterm 都行
2.3 FreeRTOS 版本的选择逻辑
FreeRTOS 目前有两个主要分支:FreeRTOS 经典版和FreeRTOS Kernel(AWS 接手后的版本)。CubeMX 内置的是 FreeRTOS Kernel,版本一般是 10.3.x 或 10.4.x。这个版本足够稳定,API 和经典版基本兼容,网上大部分教程都适用。
为什么不建议自己去 GitHub 下最新源码手动移植?因为手动移植要处理port.c、portmacro.h、内存堆配置、中断向量重定向等一堆细节,新手很容易在SysTick和PendSV中断优先级上翻车。CubeMX 生成的代码已经把这些都配好了,你直接写业务逻辑就行。等你把内核机制搞明白了,再回头手动移植也不迟。
3. 手把手搭建第一个 FreeRTOS 工程
3.1 CubeMX 工程创建与时钟配置
打开 CubeMX,点击 "New Project",在搜索框输入你的芯片型号,比如STM32F103C8,选中后点 "Start Project"。工程建好后,先别急着配 FreeRTOS,把基础的东西配好。
第一步是RCC 配置。在 "System Core" 里找到 RCC,把 High Speed Clock (HSE) 设为 "Crystal/Ceramic Resonator",也就是外部晶振。F103C8T6 板子上一般焊的是 8MHz 晶振,F407 探索板是 8MHz 或 25MHz,看原理图确认。
第二步是时钟树配置。切到 "Clock Configuration" 标签页,F103 的话把 HSE 输入 8MHz,PLL 倍频到 72MHz,然后 AHB、APB1、APB2 分别设为 72MHz、36MHz、72MHz。这里有个细节:APB1 最高只能到 36MHz,超了会不稳定。CubeMX 会自动帮你算分频系数,你只要看最终频率对不对就行。
第三步是调试接口配置。在 "System Core" 里找到 SYS,把 Debug 设为 "Serial Wire"。这一步非常关键,不配的话下载一次程序后芯片可能就锁死了,下次连 ST-Link 都识别不到。如果你不小心锁了,把 BOOT0 拉高上电,再用 ST-Link Utility 擦除全片就能救回来。
3.2 FreeRTOS 中间件配置要点
在左侧 "Middleware and Software Packs" 里找到 FREERTOS,Interface 选 "CMSIS_V1" 或 "CMSIS_V2"。这两个的区别是:V1 是传统接口,兼容性好;V2 是 CMSIS-RTOS2 标准,API 更现代。新手建议选CMSIS_V1,因为网上教程多,遇到问题好查。
选好之后切到 "Config parameters" 标签页,重点看几个参数:
- Kernel settings:
USE_PREEMPTION设为 Enabled,这是抢占式调度,高优先级任务能打断低优先级任务,是 RTOS 的核心特性。 - Memory management:
Memory Allocation选heap_4。heap_4 支持内存碎片合并,比 heap_1 和 heap_2 更实用,是项目中最常用的方案。 - Hook functions:
USE_IDLE_HOOK和USE_TICK_HOOK先关掉,等需要的时候再开。 - Software timers:
USE_TIMERS可以先关,减少资源占用。
然后在 "Tasks and Queues" 标签页,CubeMX 默认会给你创建一个defaultTask。我们可以先保留它,把优先级设为osPriorityNormal,栈大小设为 128 words(也就是 512 字节)。栈大小这个参数后面会详细讲怎么估算,先按默认来。
3.3 生成工程与编译下载
配置完成后,切到 "Project Manager" 标签页:
- Project Name 填
FreeRTOS_Day1 - Project Location 选一个没有中文和空格的路径,这点很重要,Keil 对中文路径支持很差
- Toolchain / IDE 选 "MDK-ARM",版本选你装的 Keil 版本
在 "Code Generator" 里勾上 "Generate peripheral initialization as a pair of .c/.h files",这样代码结构更清晰。然后点 "GENERATE CODE",CubeMX 会自动生成工程并打开 Keil。
在 Keil 里直接点 "Build",正常情况下应该零错误零警告。如果报错,大概率是这两个原因:一是 Keil 没装对应的器件包(Pack),去 Keil 官网下载 STM32F1xx_DFP 安装即可;二是 ARM Compiler 版本不对,在 "Options for Target" → "Target" 里把编译器切到 AC6。
编译通过后,用 ST-Link 连接板子,点 "Download" 下载程序。如果提示 "No target connected",检查一下接线:SWDIO、SWCLK、GND、3.3V 四根线必须接对,板子要单独供电或者由 ST-Link 供电。
4. 第一个任务:让 LED 闪起来
4.1 默认任务的代码结构
CubeMX 生成的main.c里,MX_FREERTOS_Init()函数会调用osThreadCreate创建默认任务。我们打开freertos.c文件,找到StartDefaultTask函数:
void StartDefaultTask(void const * argument) { for(;;) { osDelay(1); } }这个for(;;)就是任务的无限循环,osDelay(1)让任务挂起 1 个 tick。任务函数绝对不能返回,一旦返回就会触发prvTaskExitError,系统直接卡死。这是新手最容易犯的错误之一。
4.2 添加 LED 闪烁逻辑
假设你的板子上 LED 接在 PC13(F103C8T6 蓝板就是这样),先在 CubeMX 里把 PC13 配成 GPIO_Output,重新生成代码。然后在StartDefaultTask里写:
void StartDefaultTask(void const * argument) { for(;;) { HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); osDelay(500); } }osDelay(500)表示延时 500 个 tick。如果系统 tick 频率是 1000Hz(CubeMX 默认),那就是 500ms。下载运行,你应该能看到 LED 以 1Hz 的频率闪烁。
这里有个关键点要理解:osDelay和HAL_Delay有本质区别。HAL_Delay是忙等待,CPU 空转着数数,期间什么都干不了;osDelay会把当前任务挂起,让出 CPU 给其他任务,等时间到了再唤醒。这就是 RTOS 的价值所在——延时不再浪费 CPU。
4.3 创建第二个任务验证并发
为了直观感受多任务,我们再创建一个任务。在 CubeMX 的 FREERTOS 配置里,点 "Add" 新增一个任务,名字叫Task2,优先级设为osPriorityLow(比默认任务低),栈大小 128 words。重新生成代码后,在freertos.c里找到StartTask2:
void StartTask2(void const * argument) { for(;;) { printf("Task2 is running\r\n"); osDelay(1000); } }printf需要重定向到串口,在main.c里加上:
#include <stdio.h> int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); return ch; }同时确保 CubeMX 里 USART1 已经配置好,波特率 115200。烧录后打开串口助手,你应该能看到 Task2 每秒打印一次,同时 LED 还在闪。两个任务互不干扰,这就是并发。
提示:
printf重定向后,如果在多个任务里同时调用,可能会出现输出错乱。因为printf不是线程安全的。解决办法是用互斥量保护,或者每个任务用独立的缓冲区。这个问题后面讲队列和信号量的时候会详细展开。
5. 环境搭建常见问题与排查实录
5.1 编译与下载类问题
问题一:Keil 编译报 "cannot open source input file 'FreeRTOS.h'"
这是头文件路径没配好。CubeMX 生成的工程一般会自动配好,但如果你手动移动过文件就会出问题。在 Keil 的 "Options for Target" → "C/C++" → "Include Paths" 里,把Middlewares/Third_Party/FreeRTOS/Source/include和.../Source/portable/RVDS/ARM_CM3加进去。
问题二:下载后程序不运行,LED 不亮
先检查 BOOT0 和 BOOT1 跳线,正常运行应该都接地。然后确认复位电路正常,按一下复位键试试。如果还不行,用调试器单步跟踪,看程序是不是卡在HardFault_Handler里。卡死的原因通常是栈溢出或者中断优先级配置错误。
问题三:ST-Link 提示 "No target connected"
按这个顺序排查:接线是否正确(SWDIO 对应 PA13,SWCLK 对应 PA14);板子是否供电;ST-Link 驱动是否安装;芯片是否被读保护。如果被读保护了,用 ST-Link Utility 连接后执行 "Target" → "Option Bytes" → 把 Read Out Protection 设为 Disabled。
5.2 运行时异常排查
问题四:串口打印乱码
九成是波特率不匹配。检查 CubeMX 里 USART 的波特率设置和串口助手是否一致。另外 F103 的 USART1 挂在 APB2 上,时钟是 72MHz;USART2/3 挂在 APB1 上,时钟是 36MHz。如果时钟树配错,实际波特率就会偏。
问题五:系统跑一会儿就死机
最常见的原因是栈溢出。FreeRTOS 每个任务有独立的栈,如果任务里定义了大的局部数组,或者函数调用层次太深,栈就会溢出。开启configCHECK_FOR_STACK_OVERFLOW设为 2,然后在vApplicationStackOverflowHook里打个断点,就能定位到是哪个任务溢出了。
栈大小的估算方法:把任务里所有局部变量、函数调用参数、返回地址加起来,再乘以 1.5 到 2 的安全系数。比如一个任务里有个 128 字节的数组,调用了三层函数,每层 32 字节,那至少需要 (128 + 32*3) * 2 = 448 字节,也就是 112 words。CubeMX 里栈大小是以 word(4 字节)为单位的,别搞混了。
问题六:中断里调用 FreeRTOS API 导致死机
FreeRTOS 的 API 分两类:任务级和中断级。在中断服务函数里必须用带FromISR后缀的版本,比如xQueueSendFromISR而不是xQueueSend。而且中断优先级不能高于configMAX_SYSCALL_INTERRUPT_PRIORITY,否则会破坏内核临界区。Cortex-M3 的优先级数值越小优先级越高,CubeMX 默认把 FreeRTOS 可管理的中断优先级设为 5,你配置外设中断时优先级数值要大于等于 5。
5.3 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 编译报头文件找不到 | Include 路径缺失 | 检查 Keil 的 Include Paths |
| 下载失败 | 接线错误或芯片锁死 | 检查 SWD 接线,用 ST-Link Utility 解锁 |
| LED 不亮 | GPIO 配置错误或时钟未使能 | 检查 CubeMX 引脚配置 |
| 串口乱码 | 波特率或时钟配置错误 | 核对时钟树和串口助手设置 |
| 运行一段时间死机 | 栈溢出或堆耗尽 | 开启栈溢出检测,增大 heap 或任务栈 |
| 中断里死机 | 用了非 FromISR 版本 API | 改用 FromISR 版本,检查中断优先级 |
| 任务不切换 | 调度器未启动或优先级相同 | 检查osKernelStart是否调用 |
6. 一些过来人的经验之谈
环境搭建这一步,看起来是体力活,但其实藏着很多对后续开发影响深远的细节。我踩过的坑里,最值得说的有三个。
第一个是路径问题。Keil 对中文路径和空格路径的支持极差,我曾经把工程放在 "D:\我的项目\FreeRTOS 学习" 下面,编译时各种莫名其妙的错误,换成 "D:\Projects\FreeRTOS_Day1" 就全好了。所以从第一天起就养成好习惯,工程路径全英文、无空格、不要太深。
第二个是版本匹配。CubeMX 的版本、Keil 的版本、器件包的版本、FreeRTOS 的版本,这四个东西之间有兼容性要求。我遇到过 CubeMX 6.10 生成的工程在 Keil 5.30 上编译报错,升级到 Keil 5.38 就好了。所以尽量用较新的稳定版本,别用太老的。
第三个是先跑通再优化。新手容易犯的毛病是一上来就想把工程结构搞得特别完美,文件夹分得特别细,结果配置来配置去就是跑不起来。我的建议是:第一天的目标就是让 LED 闪起来、串口能打印,其他的都往后放。跑通了再慢慢重构,心里有底。
最后分享一个调试小技巧:在main.c的osKernelStart()之前加一句printf("System starting...\r\n"),如果串口能打印出来但后面没动静,说明卡在调度器启动或者第一个任务里了;如果连这句都打不出来,说明卡在更早的初始化阶段。这个简单的打印能帮你快速缩小问题范围。
明天进入第 2 天,我会讲 FreeRTOS 的任务管理机制,包括任务状态转换、优先级调度、任务通知这些核心概念,并且用实际代码演示任务之间怎么协作。环境搭好的朋友可以先自己试试创建三四个任务,观察它们的执行顺序,提前感受一下抢占式调度的行为。