2. FreeRTOS 移植:从 CubeMX 配置到第一个任务跑起来

说实话,很多初学者一听到「移植 RTOS」就觉得头皮发麻。其实没那么玄乎。说白了,就是把 FreeRTOS 的源码放到你的工程里,再把几个跟硬件相关的接口填好。我刚开始搞的时候也踩过不少坑,今天咱们就一步步来,把这事捋清楚。

2.1 移植的本质是什么?

移植 RTOS,核心就三件事:

  • 源码放对位置 —— 别让编译器找不到头文件
  • 时钟配准 —— 系统节拍(SysTick)得告诉 RTOS 时间怎么走
  • 中断处理好 —— 尤其是 PendSV 和 SVC,这俩是任务切换的命脉

我个人习惯用 CubeMX 生成基础工程,再手动把 FreeRTOS 源码加进去。这样既保留了 HAL 库的便利,又能清楚看到 RTOS 到底改了哪些地方。

移植 ≠ 重写。你不需要理解 FreeRTOS 的每一个宏,只需要知道它需要什么、你给它什么。

2.2 用 CubeMX 快速搭建骨架

打开 CubeMX,选好你的 STM32 型号(我用的是 STM32F407VET6,不过其他型号流程一样)。

  1. 配置时钟:HSE 外部晶振 8MHz,PLL 倍频到 168MHz。APB1 42MHz,APB2 84MHz。
  2. 配置调试接口:Serial Wire 模式,不然下载一次程序后芯片就锁死了——我当年就吃过这个亏。
  3. 配置一个串口:USART1,波特率 115200,用于打印调试信息。
  4. 生成代码: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 移植流程总览

下面这张图把整个流程串起来了,你可以对照着检查自己走到哪一步了:

FreeRTOS 移植流程 1. CubeMX 生成基础工程 2. 加入 FreeRTOS 源码 3. 配置 FreeRTOSConfig.h 4. 配置中断优先级 5. 编写第一个任务并运行 6. 验证清单逐项通过 常见翻车点 • 头文件路径没加全 • SysTick 优先级设太高 • heap 大小不够 • 中断里调了非 ISR 函数 核心文件清单 • tasks.c / queue.c / list.c • heap_4.c • port.c / portmacro.h • FreeRTOSConfig.h

移植这件事,说白了就是「配环境、写配置、调中断」。只要这三步走对了,RTOS 就能稳稳地跑起来。我见过太多人卡在中断优先级上,其实只要记住一条铁律:RTOS 内核相关的中断优先级最低,用户中断优先级次之,基本就不会出大问题。