4、FreeRTOS内核移植(上):添加FreeRTOS源码、修改启动文件、配置SysTick中断

好,咱们正式开始动手移植FreeRTOS。说实话,很多初学者一听到“移植”两个字就觉得头大,觉得这是大神才干的事。其实不然。你想想看,STM32的生态已经非常成熟了,官方和社区把大部分脏活累活都干完了。我们要做的,说白了就是把FreeRTOS的源码请进工程,然后告诉芯片:“嘿,以后这个定时器归RTOS管了。”

我个人习惯把移植分成三步走:加源码 → 改启动 → 配时钟。这一章我们先搞定前两步,顺便把SysTick中断配置讲透。嗯,这三步走顺了,你的FreeRTOS就能在STM32上跑起来了。

核心目标:让FreeRTOS的调度器能够在STM32上获得“心跳”,并且能够正确响应SysTick中断,完成任务切换的准备工作。

4.1 添加FreeRTOS源码到工程

这一步其实没什么技术含量,但容易出错。我见过不少同事直接把整个FreeRTOS文件夹拖进工程,结果编译报错一大堆——因为里面有很多用不到的demo文件和移植层代码。

正确的做法是:只拿我们需要的。FreeRTOS的源码结构其实很清晰,核心就两个目录:

目录/文件 说明 是否必须
Source/ 内核核心代码,包括tasks.c、queue.c、list.c等 是
Source/include/ 所有头文件,必须包含在工程头文件路径中 是
Source/portable/ 移植层代码,按编译器+MCU架构选择 是
Source/portable/MemMang/ 内存管理方案,heap_1.c ~ heap_5.c 选一个

具体到我们的STM32F1系列(Cortex-M3内核),使用Keil MDK编译器,你需要添加的文件如下:

FreeRTOS/
├── Source/
│   ├── tasks.c
│   ├── queue.c
│   ├── list.c
│   ├── timers.c
│   ├── event_groups.c
│   └── croutine.c          // 协程,一般不用,可删
│
├── Source/portable/
│   ├── RVDS/               // ARM RealView编译器
│   │   └── ARM_CM3/
│   │       ├── port.c
│   │       └── portmacro.h
│   └── MemMang/
│       └── heap_4.c        // 我推荐用这个,防碎片

我的建议:内存管理选 heap_4.c。它支持合并相邻空闲块,能有效防止内存碎片。我在一个需要长期运行的设备上吃过heap_2的亏——跑了三天,任务创建失败了。换成heap_4之后,再没出过问题。

添加完文件后,别忘了在Keil的C/C++编译器选项中,把 FreeRTOS/Source/include 路径加到头文件搜索目录里。这一步漏了,编译会报“找不到头文件”,别问我怎么知道的……

4.2 修改启动文件——让FreeRTOS接管PendSV和SVC

好,源码加完了。但这时候编译,大概率会报错。为什么?因为FreeRTOS需要两个关键中断向量:SVC(系统服务调用)和 PendSV(可挂起系统调用)。

在裸机工程里,这两个中断的入口函数是弱定义的,或者干脆是空的。FreeRTOS需要把它们指向自己的处理函数。

具体来说,你要在启动文件(比如 startup_stm32f10x_hd.s)里,找到这两行:

DCD     SVC_Handler             ; SVC Handler
DCD     PendSV_Handler          ; PendSV Handler

把它们改成:

DCD     vPortSVCHandler         ; SVC Handler
DCD     xPortPendSVHandler      ; PendSV Handler

为什么要改?因为FreeRTOS的移植层代码(port.c)里,已经实现了这两个中断服务函数。如果你不改,系统启动后调用SVC指令时,会跳转到你原来的空函数,调度器根本跑不起来。

注意:有些教程会让你在工程里重新定义 SVC_Handler 和 PendSV_Handler 函数,用强符号覆盖弱符号。这种做法也能用,但我个人更推荐直接改启动文件——干净、直观,以后排查问题的时候一眼就能看出来。

改完之后,记得把启动文件里的 SVC_Handler 和 PendSV_Handler 的弱定义注释掉,避免重复定义。嗯,这一步很容易忘,我踩过这个坑。

4.3 配置SysTick中断——给RTOS一颗“心脏”

FreeRTOS需要一个周期性的时基(tick),用来做任务调度和时间管理。这个时基通常由SysTick定时器提供。SysTick是Cortex-M内核自带的24位递减计数器,简单可靠,不用额外配置外设定时器。

配置SysTick,其实就是在 FreeRTOSConfig.h 里定义好时钟频率,然后在 port.c 里调用初始化函数。但这里有个关键点:SysTick的中断优先级。

FreeRTOS要求SysTick的中断优先级必须设置为 最低优先级。为什么?因为调度器在临界区(比如操作队列、创建任务)时会关闭部分中断,如果SysTick优先级太高,它可能会打断临界区操作,导致数据不一致。

具体配置如下:

// FreeRTOSConfig.h 中配置时钟
#define configCPU_CLOCK_HZ          ( ( unsigned long ) 72000000 )  // STM32F1主频72MHz
#define configTICK_RATE_HZ          ( ( TickType_t ) 1000 )         // 1ms一个tick
#define configSYSTICK_CLOCK_HZ      ( configCPU_CLOCK_HZ / 8 )     // SysTick时钟源,通常为HCLK/8

然后在 port.c 中,FreeRTOS会调用 vPortSetupTimerInterrupt() 来初始化SysTick。这个函数内部会设置SysTick的重装载值、时钟源,以及中断优先级。

如果你用的是标准库或者HAL库,也可以手动初始化SysTick,但要注意:不要和HAL库的HAL_Init()冲突。HAL库默认也会初始化SysTick作为时基,优先级是0(最高)。如果你直接跑FreeRTOS,两个SysTick中断会打架。

避坑指南:我曾经在一个项目里,用了HAL库的 HAL_Delay(),同时又跑了FreeRTOS。结果发现任务调度偶尔会卡死。查了两天才发现,HAL库把SysTick优先级设成了0,FreeRTOS的临界区关不掉它,导致队列操作被中断破坏。解决方案:要么把HAL库的SysTick优先级也改成最低,要么干脆不用HAL_Delay,改用FreeRTOS的 vTaskDelay()。

4.4 本章知识结构图

下面这张图,帮你把整个移植流程串起来。从源码添加到启动文件修改,再到SysTick配置,每一步的输入输出都很清楚。

FreeRTOS内核移植流程(上) 添加FreeRTOS源码 Source/ + portable/ + MemMang/ 修改启动文件 SVC → vPortSVCHandler PendSV → xPortPendSVHandler 配置SysTick中断 优先级设为最低 关键配置项(FreeRTOSConfig.h) • configCPU_CLOCK_HZ = 72000000 // STM32F1主频 • configTICK_RATE_HZ = 1000 // 1ms tick • configSYSTICK_CLOCK_HZ = HCLK/8 // SysTick时钟源 常见问题 • 编译报错:头文件路径未添加 • 链接报错:SVC/PendSV重复定义 • 运行时卡死:SysTick优先级冲突 推荐做法 • 内存管理用 heap_4.c • 直接改启动文件,不用弱符号覆盖 • 避免HAL_Delay与FreeRTOS共用SysTick 完成这三步,FreeRTOS就能在STM32上获得稳定的“心跳”

4.5 验证移植是否成功

配置完成后,怎么知道移植成功了?最简单的方法:写一个空任务,然后启动调度器。如果程序能跑起来,SysTick中断正常触发,说明移植基本没问题。

这里给一个最小验证代码:

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

void vTask1(void *pvParameters)
{
    while(1)
    {
        // 任务循环,暂时什么都不做
    }
}

int main(void)
{
    // 硬件初始化(时钟、GPIO等)
    // ...
    
    // 创建任务
    xTaskCreate(vTask1, "Task1", 128, NULL, 1, NULL);
    
    // 启动调度器
    vTaskStartScheduler();
    
    // 正常情况下不会执行到这里
    while(1);
}

如果程序卡在 vTaskStartScheduler() 之后不动了,或者直接跑飞,别慌。先检查这几样:

  • SysTick中断是否使能:在调试模式下,查看SysTick的CTRL寄存器,确认ENABLE位为1。
  • 中断优先级是否正确:FreeRTOS要求SysTick优先级为最低(数值最大),检查NVIC的优先级分组是否匹配。
  • PendSV和SVC向量是否指向正确函数:在启动文件中确认。

重要提醒:如果你用的是STM32CubeMX生成的工程,它默认会初始化SysTick并设置优先级为0。这时候直接跑FreeRTOS,调度器会异常。解决办法:在 HAL_Init() 之后,手动调用 HAL_SuspendTick() 暂停HAL的SysTick,然后让FreeRTOS接管。