4. FreeRTOS核心概念

好,咱们正式开始动手移植。这一章我带你走一遍完整的移植流程,重点放在 FreeRTOSConfig.h 这个配置文件的修改上。说实话,很多初学者卡就卡在这个文件上——配置不对,系统要么跑不起来,要么功耗高得吓人。

我个人习惯把移植分成两大块:复制文件 和 修改配置。复制文件是体力活,改配置才是技术活。咱们先干体力活,再啃硬骨头。

4.1 移植前的准备工作

动手之前,你得先准备好三样东西:

  • 一个能正常运行的STM32裸机工程(我用的是STM32F103和STM32F407各一套)
  • FreeRTOS官方源码包(去官网下载V10.4.6或更新版本)
  • 你的开发板原理图(主要是为了确认晶振频率和中断优先级分组)
我的小习惯: 我会在裸机工程里先跑一个LED闪烁程序,确认时钟、GPIO、SysTick都正常。这样移植后如果出问题,能快速定位是FreeRTOS的问题还是硬件的问题。

4.2 移植步骤详解

移植说白了就三步:复制 → 包含 → 修改。我画了张流程图,你先看个全貌。

FreeRTOS内核移植流程(STM32F1/F4) 步骤1:复制FreeRTOS源码到工程 步骤2:添加头文件路径到IDE 步骤3:修改FreeRTOSConfig.h 步骤4:编写port.c中断处理函数 步骤5:编译验证,运行测试任务 核心:步骤3是移植成败的关键,配置错误会导致系统无法启动或任务调度异常

4.3 复制文件到工程目录

打开你下载的FreeRTOS源码包,找到 FreeRTOS/Source 目录。你需要复制以下文件:

源文件/文件夹 复制到工程的位置 说明
tasks.c FreeRTOS/Source/ 任务管理核心
list.c FreeRTOS/Source/ 链表操作
queue.c FreeRTOS/Source/ 队列和信号量
timers.c FreeRTOS/Source/ 软件定时器
event_groups.c FreeRTOS/Source/ 事件组
portable/MemMang/heap_4.c FreeRTOS/Source/portable/MemMang/ 内存管理(推荐heap_4)
portable/RVDS/ARM_CM3/ 或 ARM_CM4F/ FreeRTOS/Source/portable/ 移植层代码(F1选CM3,F4选CM4F)
include/ 文件夹 FreeRTOS/Source/include/ 所有头文件
注意: F4系列带FPU,必须使用 ARM_CM4F 目录下的port.c和portmacro.h。用错了会导致任务切换时浮点寄存器没保存,计算结果全乱套。我曾经帮一个客户排查了三天,最后发现就是port文件选错了。

4.4 添加头文件路径

文件复制完后,打开你的IDE(我习惯用Keil,但MDK、IAR、STM32CubeIDE都一样),在工程设置里添加以下头文件路径:

  • FreeRTOS/Source/include
  • FreeRTOS/Source/portable/RVDS/ARM_CM3(F1)或 ARM_CM4F(F4)
  • 你的工程主目录(放FreeRTOSConfig.h的地方)

嗯,这一步没什么技术含量,但漏掉一个路径编译就会报一堆"file not found"。我建议你加完路径后先编译一次,确认所有头文件都能找到。

4.5 修改FreeRTOSConfig.h——核心中的核心

好了,重头戏来了。FreeRTOSConfig.h 是FreeRTOS的"性格"配置文件。你想想看,系统用多大的堆栈、支持多少个优先级、要不要用钩子函数——全在这里定义。

FreeRTOS官方提供了一个模板文件,在 FreeRTOS/Demo/CORTEX_STM32F103_Keil 目录下。我建议你拿这个模板改,别自己从头写。

下面是我针对STM32F1/F4系列整理的关键配置项,你直接照着改就行:

配置宏 F1推荐值 F4推荐值 说明
configCPU_CLOCK_HZ 72000000 168000000 必须与你的系统时钟一致
configTICK_RATE_HZ 1000 1000 1ms一个tick,够用
configMAX_PRIORITIES 5 5 够用就行,别贪多
configMINIMAL_STACK_SIZE 128 128 单位是word,不是byte
configTOTAL_HEAP_SIZE 10240 20480 根据你的RAM大小调整
configUSE_PREEMPTION 1 1 抢占式调度
configUSE_IDLE_HOOK 0 0 低功耗时才需要
configUSE_TICK_HOOK 0 0 一般用不到
configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15 15 STM32用4位优先级,最低是15
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 5 优先级高于5的中断不能调用FreeRTOS API
关键点: configCPU_CLOCK_HZ 必须和你工程里的系统时钟一致。F1默认是72MHz,F4默认是168MHz。如果你用了外部晶振或者改了PLL倍频,这里一定要同步修改。我见过有人F4配了168MHz,但实际跑在180MHz,结果tick时间全乱套。

下面是我常用的一个配置片段,你直接复制到你的 FreeRTOSConfig.h 里:

/* 系统时钟和Tick配置 */
#define configCPU_CLOCK_HZ                 ((unsigned long)72000000)  // F1用72MHz,F4改成168000000
#define configTICK_RATE_HZ                 ((TickType_t)1000)
#define configUSE_PREEMPTION               1
#define configUSE_IDLE_HOOK                0
#define configUSE_TICK_HOOK                0

/* 任务和堆栈配置 */
#define configMAX_PRIORITIES               (5)
#define configMINIMAL_STACK_SIZE           ((unsigned short)128)
#define configTOTAL_HEAP_SIZE              ((size_t)(10 * 1024))  // F4建议20KB
#define configMAX_TASK_NAME_LEN            (16)

/* 中断优先级配置(STM32专用) */
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY      15
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
#define configKERNEL_INTERRUPT_PRIORITY              configLIBRARY_LOWEST_INTERRUPT_PRIORITY
#define configMAX_SYSCALL_INTERRUPT_PRIORITY         configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY

/* 功能开关 */
#define configUSE_MUTEXES                  1
#define configUSE_RECURSIVE_MUTEXES        1
#define configUSE_COUNTING_SEMAPHORES      1
#define configUSE_QUEUE_SETS               0
#define configUSE_TASK_NOTIFICATIONS       1

4.6 中断优先级配置——最容易踩坑的地方

STM32的中断优先级分组和FreeRTOS的配合,是移植中最容易出问题的地方。我专门说一下。

STM32F1/F4默认使用4位优先级分组(NVIC_PriorityGroup_4),也就是4位全部用于抢占优先级,没有子优先级。FreeRTOS要求中断优先级分组必须是4位抢占优先级模式。

你在系统初始化时一定要调用:

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);

为什么?因为FreeRTOS内部用 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 来判断一个中断能不能调用FreeRTOS API。如果优先级分组不对,这个判断逻辑就全错了。

我曾经踩过的坑: 有一次我在F407上移植,忘了设置优先级分组,默认是0位抢占+4位子优先级。结果SysTick中断优先级设成了0,但FreeRTOS以为它是15。任务切换的时候,该保存的寄存器没保存,程序跑飞了。查了两天才发现是优先级分组的问题。

4.7 编写中断处理函数

FreeRTOS接管了SysTick和PendSV两个中断。你需要把裸机工程里的 SysTick_Handler 替换成FreeRTOS的版本:

#include "FreeRTOS.h"
#include "task.h"

void SysTick_Handler(void)
{
    if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
    {
        xPortSysTickHandler();
    }
}

void PendSV_Handler(void)
{
    vPortPendSVHandler();
}

void SVC_Handler(void)
{
    vPortSVCHandler();
}

注意:如果你的工程里原来有 SysTick_Handler 函数(比如HAL库生成的),一定要把它注释掉,否则会重复定义。

4.8 编译验证

所有文件加好、配置改好之后,编译一次。如果报错,99%是以下三个原因:

  1. 头文件路径没加全——检查include路径
  2. 中断处理函数重复定义——检查startup文件里的弱定义
  3. configCPU_CLOCK_HZ和实际时钟不匹配——用示波器或者调试器确认系统时钟

编译通过后,写一个简单的测试任务:

void vTestTask(void *pvParameters)
{
    while(1)
    {
        GPIO_ToggleBits(GPIOA, GPIO_Pin_5);  // 翻转LED
        vTaskDelay(pdMS_TO_TICKS(500));      // 延时500ms
    }
}

int main(void)
{
    // 硬件初始化...
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
    
    xTaskCreate(vTestTask, "Test", configMINIMAL_STACK_SIZE, NULL, 1, NULL);
    vTaskStartScheduler();
    
    while(1);  // 正常情况下不会跑到这里
}

如果LED以500ms的间隔闪烁,恭喜你,移植成功了!

调试小技巧: 如果LED不闪,先检查 vTaskDelay 的延时时间。把 pdMS_TO_TICKS(500) 改成 500(假设tick是1ms),排除宏定义的问题。如果还不闪,用调试器在 vTaskStartScheduler 之后设断点,看看程序是不是卡在启动调度器那一步。