3. FreeRTOS移植与配置:在STM32平台上移植FreeRTOS

好,咱们进入实战环节。这一章我带你手把手把FreeRTOS移植到STM32上。说实话,移植这事儿看着复杂,其实套路固定。你只要摸清几个关键点,半小时就能搞定。

3.1 移植前的准备工作

先说说我个人的习惯。我一般会从FreeRTOS官网下载最新的源码包,或者直接用STM32CubeMX生成。两种方式我都用过,但如果你刚开始接触,我建议用CubeMX——省事,不容易漏文件。

你需要准备这些东西:

  • 一个STM32开发板(我用的是STM32F407,但F1/F4系列都行)
  • Keil MDK或STM32CubeIDE(我习惯用Keil)
  • FreeRTOS v10.x源码(别用太老的版本,坑多)
  • 一个串口调试工具(方便看打印信息)
我的小建议: 第一次移植别选太复杂的板子。STM32F103C8T6这种“蓝色药丸”板就挺好,资源够用,文档也多。

3.2 核心文件结构

FreeRTOS的源码结构其实很清晰。我把它分成三块:

目录/文件 作用 是否需要修改
Source/tasks.c 任务管理核心
Source/list.c 链表操作
Source/queue.c 队列与信号量
Source/portable/ 平台相关代码 是(选对架构)
Source/include/ 头文件
FreeRTOSConfig.h 配置文件 是(必须改)

嗯,这里要注意。portable目录下有很多子目录,你得选对架构。STM32F4是Cortex-M4,所以选portable/GCC/ARM_CM4F或者portable/RVDS/ARM_CM4F,看你用什么编译器。

我曾经踩过的坑: 有一次我图省事,直接把CM3的移植文件用在了CM4上。结果任务切换时偶尔会死机。查了两天才发现是浮点寄存器保存的问题。Cortex-M4有FPU,移植文件必须带“F”后缀。

3.3 配置时钟节拍

时钟节拍是RTOS的心跳。说白了,就是系统每隔固定时间产生一次中断,让内核有机会检查要不要切换任务。

在FreeRTOS里,时钟节拍由configTICK_RATE_HZ这个宏控制。我一般设成1000,也就是1ms一个tick。

// FreeRTOSConfig.h 中的关键配置
#define configTICK_RATE_HZ          ((TickType_t)1000)   // 1ms一个tick
#define configCPU_CLOCK_HZ          ((unsigned long)168000000)  // STM32F407主频168MHz

为什么会是1000?你想想看,如果设成100,那10ms才切换一次任务,响应太慢了。如果设成10000,中断太频繁,CPU光切换任务了,正经事干不了。1000是个平衡点。

时钟节拍的实现依赖硬件定时器。在STM32上,通常用SysTick定时器。FreeRTOS的移植层已经帮我们写好了vPortSetupTimerInterrupt()这个函数,它会自动配置SysTick。

关键点: 如果你用CubeMX生成代码,记得在HAL_Init()之后调用vTaskStartScheduler()。SysTick的优先级要设成最低(数值最大),否则会影响其他中断的响应。

3.4 任务堆栈大小设置

任务堆栈大小,这是个容易出问题的地方。我见过太多人因为堆栈设小了,程序跑着跑着就莫名其妙地死机。

每个任务都有自己的堆栈,用于保存局部变量、函数调用上下文等。FreeRTOS用configMINIMAL_STACK_SIZE定义最小堆栈,通常给空闲任务用。

// 堆栈大小配置
#define configMINIMAL_STACK_SIZE    ((unsigned short)128)  // 最小堆栈,单位是字(4字节)
#define configTOTAL_HEAP_SIZE       ((size_t)(20 * 1024))  // 总堆大小20KB

// 创建任务时指定堆栈
xTaskCreate(
    vTaskFunction,      // 任务函数
    "Task1",            // 任务名
    256,                // 堆栈大小,256字 = 1KB
    NULL,               // 参数
    1,                  // 优先级
    NULL                // 任务句柄
);

堆栈大小怎么估算?我有个土办法:先设一个较大的值(比如512字),跑起来后用uxTaskGetStackHighWaterMark()查看实际使用量,再调整到1.5倍左右。

避坑指南: 我曾经在项目里遇到过一个诡异的问题——任务运行几天后突然挂掉。查到最后发现是堆栈溢出,但溢出的不是当前任务,而是另一个优先级更低的任务。因为高优先级任务不断抢占,低优先级任务的堆栈被慢慢蚕食。所以,每个任务的堆栈都要留够余量。

3.5 调度策略选择

FreeRTOS支持两种调度策略:抢占式和协作式。默认是抢占式,这也是绝大多数场景的选择。

策略 宏定义 特点 适用场景
抢占式 configUSE_PREEMPTION = 1 高优先级任务随时可以打断低优先级任务 大多数实时系统
协作式 configUSE_PREEMPTION = 0 任务主动让出CPU(调用taskYIELD()) 简单轮询、低功耗场景

我个人几乎只用抢占式。为什么?因为售货机这种系统,你想想看,用户投币后必须立刻响应,不能等当前任务慢慢执行完。抢占式能保证紧急任务第一时间拿到CPU。

// FreeRTOSConfig.h 中的调度策略配置
#define configUSE_PREEMPTION        1   // 1:抢占式  0:协作式
#define configUSE_TIME_SLICING      1   // 时间片轮转,同优先级任务轮流执行
#define configUSE_PORT_OPTIMISED_TASK_SELECTION  1  // 使用硬件优化任务选择

这里还有个configUSE_TIME_SLICING,它控制同优先级任务是否按时间片轮转。我建议打开,否则同优先级的任务里,如果有一个不主动让出CPU,其他任务就永远没机会运行。

注意: 抢占式调度下,要注意优先级反转问题。比如低优先级任务拿了锁,高优先级任务等锁,中间优先级任务却一直在跑。解决办法是用互斥量(带优先级继承的),或者干脆避免在中断和任务间共享资源。

3.6 完整的FreeRTOSConfig.h示例

下面是我常用的配置模板,你可以直接拿来用:

#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H

// 基础配置
#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 configUSE_MUTEXES                   1
#define configUSE_RECURSIVE_MUTEXES         1
#define configUSE_COUNTING_SEMAPHORES       1
#define configUSE_ALTERNATIVE_API           0
#define configQUEUE_REGISTRY_SIZE           8
#define configUSE_QUEUE_SETS                0
#define configUSE_TIME_SLICING              1
#define configUSE_NEWLIB_REENTRANT          0
#define configENABLE_BACKWARD_COMPATIBILITY 0
#define configNUM_THREAD_LOCAL_STORAGE_POINTERS 5

// 内存管理
#define configSUPPORT_STATIC_ALLOCATION     0
#define configSUPPORT_DYNAMIC_ALLOCATION    1
#define configAPPLICATION_ALLOCATED_HEAP    0

// 协程(一般不用)
#define configUSE_CO_ROUTINES               0
#define configMAX_CO_ROUTINE_PRIORITIES     2

// 中断优先级
#define configKERNEL_INTERRUPT_PRIORITY     255
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 191
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5

// 断言
#define configASSERT( x ) if( ( x ) == 0 ) { taskDISABLE_INTERRUPTS(); for( ;; ); }

#endif /* FREERTOS_CONFIG_H */

这个配置我用了好几年,在多个项目里验证过。你根据自己的芯片资源调整configTOTAL_HEAP_SIZEconfigMAX_PRIORITIES就行。

3.7 移植后的验证

移植完别急着写业务代码。先跑个简单的测试,确认系统能正常工作:

void vTestTask(void *pvParameters)
{
    while(1)
    {
        printf("Task is running, tick = %d\n", xTaskGetTickCount());
        vTaskDelay(pdMS_TO_TICKS(1000));  // 延时1秒
    }
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();
    
    xTaskCreate(vTestTask, "Test", 256, NULL, 1, NULL);
    vTaskStartScheduler();  // 启动调度器
    
    // 正常情况下不会执行到这里
    while(1);
}

如果串口能每秒打印一次tick值,恭喜你,移植成功了。如果没反应,先检查SysTick中断有没有使能,再检查vApplicationTickHook有没有定义(如果开启了tick hook)。

最后说一句: 移植FreeRTOS其实不难,难的是理解它背后的调度机制。你把这个基础打牢了,后面写多任务程序就会很顺手。下一章我们讲任务创建与管理,到时候你会看到,有了RTOS,写并发程序就像搭积木一样简单。