2. FreeRTOS 移植:从 CubeMX 配置到第一个任务跑起来
说实话,很多初学者一听到「移植 RTOS」就觉得头皮发麻。其实没那么玄乎。说白了,就是把 FreeRTOS 的源码放到你的工程里,再把几个跟硬件相关的接口填好。我刚开始搞的时候也踩过不少坑,今天咱们就一步步来,把这事捋清楚。
2.1 移植的本质是什么?
移植 RTOS,核心就三件事:
- 源码放对位置 —— 别让编译器找不到头文件
- 时钟配准 —— 系统节拍(SysTick)得告诉 RTOS 时间怎么走
- 中断处理好 —— 尤其是 PendSV 和 SVC,这俩是任务切换的命脉
我个人习惯用 CubeMX 生成基础工程,再手动把 FreeRTOS 源码加进去。这样既保留了 HAL 库的便利,又能清楚看到 RTOS 到底改了哪些地方。
移植 ≠ 重写。你不需要理解 FreeRTOS 的每一个宏,只需要知道它需要什么、你给它什么。
2.2 用 CubeMX 快速搭建骨架
打开 CubeMX,选好你的 STM32 型号(我用的是 STM32F407VET6,不过其他型号流程一样)。
- 配置时钟:HSE 外部晶振 8MHz,PLL 倍频到 168MHz。APB1 42MHz,APB2 84MHz。
- 配置调试接口:Serial Wire 模式,不然下载一次程序后芯片就锁死了——我当年就吃过这个亏。
- 配置一个串口:USART1,波特率 115200,用于打印调试信息。
- 生成代码:Toolchain 选 MDK-ARM 或 STM32CubeIDE,看你习惯。
嗯,这里要注意:CubeMX 生成的代码默认不带 RTOS。我们只是用它把外设和时钟初始化好,省得自己手写寄存器。
2.3 手动加入 FreeRTOS 源码
从 FreeRTOS 官网下载源码包(我推荐 V10.4.6 或更新版本)。把下面这些文件复制到你的工程目录:
| 文件夹 | 必须包含的文件 | 说明 |
|---|---|---|
| Source | tasks.c, queue.c, list.c, timers.c, event_groups.c, croutine.c | 内核核心文件,全部加入 |
| Source/portable/MemMang | heap_4.c | 内存管理方案,heap_4 支持碎片合并,推荐 |
| Source/portable/RVDS/ARM_CM4F | port.c, portmacro.h | 与 Cortex-M4F 对应的移植层 |
| Source/include | 所有 .h 文件 | 头文件路径要加进去 |
小技巧:把 FreeRTOSConfig.h 放在工程根目录的 Inc 文件夹下,别跟源码混在一起。这样以后改配置一目了然。
2.4 配置 FreeRTOSConfig.h —— 这是关键
这个文件是 RTOS 的「性格设定」。我见过有人直接复制网上的配置,结果任务跑起来各种诡异。你想想看,每个芯片的 Flash 大小、RAM 大小都不一样,配置能一样吗?
下面是我常用的配置模板,注释里写了为什么这么设:
#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H
/* 使用 CMSIS 的 NVIC 优先级定义 */
#define configUSE_PREEMPTION 1
#define configUSE_IDLE_HOOK 0
#define configUSE_TICK_HOOK 0
#define configCPU_CLOCK_HZ ( ( unsigned long ) 168000000 )
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
#define configMAX_PRIORITIES ( 5 )
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 )
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 20 * 1024 ) )
#define configMAX_TASK_NAME_LEN ( 16 )
#define configUSE_TRACE_FACILITY 1
#define configUSE_16_BIT_TICKS 0
#define configIDLE_SHOULD_YIELD 1
/* 中断优先级配置 —— 非常重要 */
#define configPRIO_BITS 4
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 0xf
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
#define configKERNEL_INTERRUPT_PRIORITY \
( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define configMAX_SYSCALL_INTERRUPT_PRIORITY \
( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
/* 可选功能 */
#define INCLUDE_vTaskDelay 1
#define INCLUDE_xTaskGetTickCount 1
#define INCLUDE_vTaskDelete 1
#endif /* FREERTOS_CONFIG_H */
我曾经把 configTOTAL_HEAP_SIZE 设成 2KB,结果创建第三个任务时直接崩了。后来才意识到,每个任务至少需要 128 字的栈空间,再加上 TCB 开销,20KB 起步比较稳妥。
2.5 中断优先级 —— 最容易翻车的地方
FreeRTOS 要求所有使用 RTOS API 的中断,其优先级必须低于 configMAX_SYSCALL_INTERRUPT_PRIORITY。为什么?
因为 RTOS 在临界区时会屏蔽低于这个优先级的中断。如果你把某个中断设得比这个值高,它就能打断临界区,然后你在中断里调用了 RTOS API……嗯,死锁就是这么来的。
STM32 的 NVIC 优先级分组通常设为 4 位抢占优先级(Group 4)。这时优先级值范围是 0~15,数值越小优先级越高。我一般这样设:
- SysTick 中断:优先级 15(最低,因为它是 RTOS 的节拍)
- PendSV 中断:优先级 15(同样最低)
- SVC 中断:优先级 15
- 用户中断(如串口、定时器):优先级 5~14
这样 RTOS 内核永远不会被用户中断阻塞,而用户中断只要优先级不低于 5,就能安全调用 xQueueSendFromISR 这类函数。
2.6 第一个任务:让 LED 闪起来
配置好了,咱们写个最简单的任务验证一下。在 main.c 里加入:
/* 任务句柄 */
TaskHandle_t xLEDTaskHandle = NULL;
/* 任务函数 */
void vLEDTask(void *pvParameters)
{
(void)pvParameters; /* 消除未使用参数的警告 */
/* 初始化 LED 引脚 —— 假设是 PA0 */
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
for (;;)
{
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0);
vTaskDelay(pdMS_TO_TICKS(500)); /* 延时 500ms */
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
/* 创建任务 */
xTaskCreate(
vLEDTask, /* 任务函数 */
"LED", /* 任务名 */
128, /* 栈大小,单位是字 */
NULL, /* 参数 */
1, /* 优先级 */
&xLEDTaskHandle /* 任务句柄 */
);
/* 启动调度器 —— 从这里开始,控制权交给 RTOS */
vTaskStartScheduler();
/* 正常情况下不会执行到这里 */
for (;;);
}
编译下载,如果 LED 以 0.5 秒间隔闪烁,恭喜你,移植成功了。
如果灯不亮,先检查硬件连接,再用调试器在 vTaskStartScheduler 处设断点。如果程序跑飞了,八成是栈溢出或中断优先级没配好。
2.7 移植后的验证清单
我每次移植完都会跑一遍这个清单,确保 RTOS 真的在工作:
| 检查项 | 验证方法 | 预期结果 |
|---|---|---|
| SysTick 中断 | 在 SysTick_Handler 里设断点 | 每 1ms 触发一次 |
| 任务切换 | 创建两个不同优先级的任务,分别翻转不同 LED | 高优先级任务先运行 |
| 阻塞延时 | 用 vTaskDelay 延时,观察 LED 闪烁周期 | 周期准确(误差 < 1%) |
| 临界区保护 | 在中断中调用 xQueueSendFromISR | 不卡死、不死锁 |
| 栈溢出检测 | 将任务栈设得很小(如 64 字) | 触发 configCHECK_FOR_STACK_OVERFLOW 钩子 |
2.8 移植流程总览
下面这张图把整个流程串起来了,你可以对照着检查自己走到哪一步了:
移植这件事,说白了就是「配环境、写配置、调中断」。只要这三步走对了,RTOS 就能稳稳地跑起来。我见过太多人卡在中断优先级上,其实只要记住一条铁律:RTOS 内核相关的中断优先级最低,用户中断优先级次之,基本就不会出大问题。